Settings: controls give way, notices fold, and every edit needs its owner
A settings row's control slot shrinks to the box, so a control's text wraps and a palette's row folds; the font size's sample wraps in a plain block. A notice's boxes carry their channel's name and fold under the kind's name when narrow. canvas::panel::editing takes the owner's handler itself: the design panels, the structure editor, the index manager and the privacy, notice and startup settings take theirs at render and fail loud without one. Apply fails loud if pressed while the proxy does not read. Tests cover Apply resting on a bad port, the way back resting at the default size and a box pressed off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
93de2a6bd6
commit
9a8dbcb92e
15 files changed
+172
-85
No files matched your search
@@ -236,10 +236,14 @@ impl RenderOnce for FontSizeControl {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(Pixels::from(self.size))
|
||||
.text_color(theme.colors.fg)
|
||||
.child("The quick brown fox jumps over the lazy dog."),
|
||||
div().flex().child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(Pixels::from(self.size))
|
||||
.text_color(theme.colors.fg)
|
||||
.child("The quick brown fox jumps over the lazy dog."),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,10 @@ impl RenderOnce for ProxySettings {
|
||||
"manual" => manual(&text(&host), &text(&port), &text(&bypass)),
|
||||
other => panic!("proxy settings: no way named {other}"),
|
||||
};
|
||||
let (switched, on_apply) = (fields.clone(), self.on_apply);
|
||||
let on_apply = self
|
||||
.on_apply
|
||||
.unwrap_or_else(|| panic!("proxy settings {id:?} has no on_apply"));
|
||||
let switched = fields.clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -181,10 +184,9 @@ impl RenderOnce for ProxySettings {
|
||||
.variant(ButtonVariant::Primary)
|
||||
.disabled(read.is_none())
|
||||
.on_click(move |_, window, cx| {
|
||||
if let (Some(proxy), Some(on_apply)) = (read.clone(), &on_apply) {
|
||||
log::info!("proxy settings: apply {proxy:?}");
|
||||
on_apply(proxy, window, cx);
|
||||
}
|
||||
let proxy = read.clone().expect("Apply rests until the proxy reads");
|
||||
log::info!("proxy settings: apply {proxy:?}");
|
||||
on_apply(proxy, window, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
+44
-39
@@ -46,7 +46,10 @@ impl PrivacySettings {
|
||||
|
||||
impl RenderOnce for PrivacySettings {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let (id, privacy, on_change) = (self.id, self.privacy, &self.on_change);
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("privacy settings {:?} has no on_change", self.id));
|
||||
let (id, privacy, on_change) = (self.id, self.privacy, &on_change);
|
||||
let row = |key: &'static str,
|
||||
title: &'static str,
|
||||
text: &'static str,
|
||||
@@ -100,7 +103,7 @@ pub struct Notices {
|
||||
pub quiet: bool,
|
||||
}
|
||||
|
||||
/// A grid of notices: a row for each kind, a box for each channel it may arrive by, and a switch that holds every notice back.
|
||||
/// Notices by kind: a row for each, a named box for each channel it may arrive by that folds under the name when narrow, and a switch that holds every notice back.
|
||||
#[derive(IntoElement)]
|
||||
pub struct NotificationSettings {
|
||||
id: ElementId,
|
||||
@@ -125,55 +128,54 @@ impl NotificationSettings {
|
||||
|
||||
impl RenderOnce for NotificationSettings {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (id, notices, on_change) = (self.id, self.notices, &self.on_change);
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("notification settings {:?} has no on_change", self.id));
|
||||
let (id, notices, on_change) = (self.id, self.notices, &on_change);
|
||||
let theme = cx.theme();
|
||||
let cell = theme.label_width() * 0.5;
|
||||
let head = div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(theme.colors.fg_subtle)
|
||||
.child(div().flex_1().min_w_0())
|
||||
.children(notices.channels.iter().map(|channel| {
|
||||
div()
|
||||
.flex_none()
|
||||
.w(cell)
|
||||
.child(Ellipsis::new(channel.clone()))
|
||||
}));
|
||||
let rows = notices.kinds.iter().map(|(kind, name)| {
|
||||
let boxes = notices.channels.iter().map(|channel| {
|
||||
let pair = (kind.clone(), channel.clone());
|
||||
let on = notices.on.contains(&pair);
|
||||
let set = editing(
|
||||
"notifications",
|
||||
¬ices,
|
||||
on_change,
|
||||
move |notices: &mut Notices, on: bool| {
|
||||
notices.on.retain(|each| *each != pair);
|
||||
if on {
|
||||
notices.on.push(pair.clone());
|
||||
}
|
||||
},
|
||||
);
|
||||
Checkbox::new((id.clone(), format!("{kind}-{channel}")), on)
|
||||
.label(channel.clone())
|
||||
.on_change(set)
|
||||
});
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.gap_x_4()
|
||||
.gap_y_1()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors.border)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.min_w(theme.label_width())
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(Ellipsis::new(name.clone())),
|
||||
)
|
||||
.children(notices.channels.iter().map(|channel| {
|
||||
let pair = (kind.clone(), channel.clone());
|
||||
let on = notices.on.contains(&pair);
|
||||
let set = editing(
|
||||
"notifications",
|
||||
¬ices,
|
||||
on_change,
|
||||
move |notices: &mut Notices, on: bool| {
|
||||
notices.on.retain(|each| *each != pair);
|
||||
if on {
|
||||
notices.on.push(pair.clone());
|
||||
}
|
||||
},
|
||||
);
|
||||
div().flex_none().w(cell).child(
|
||||
Checkbox::new((id.clone(), format!("{kind}-{channel}")), on).on_change(set),
|
||||
)
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.flex_none()
|
||||
.gap_x_3()
|
||||
.children(boxes),
|
||||
)
|
||||
});
|
||||
let quiet = editing(
|
||||
"notifications",
|
||||
@@ -188,7 +190,7 @@ impl RenderOnce for NotificationSettings {
|
||||
.description("Hold every notice back until it is off.")
|
||||
.control(Switch::new((id.clone(), "quiet"), notices.quiet).on_change(quiet)),
|
||||
)
|
||||
.row(div().pt_3().child(head).children(rows))
|
||||
.row(div().pt_3().children(rows))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +234,10 @@ impl StartupSettings {
|
||||
|
||||
impl RenderOnce for StartupSettings {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (id, startup, on_change) = (self.id, self.startup, &self.on_change);
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("startup settings {:?} has no on_change", self.id));
|
||||
let (id, startup, on_change) = (self.id, self.startup, &on_change);
|
||||
let switch = |key: &'static str, on: bool, set: fn(&mut Startup, bool)| {
|
||||
Switch::new((id.clone(), key), on)
|
||||
.on_change(editing("startup", &startup, on_change, set))
|
||||
|
||||
@@ -217,6 +217,9 @@ impl RenderOnce for SettingsRow {
|
||||
.child(text)
|
||||
})),
|
||||
)
|
||||
.children(self.control.map(|control| div().flex_none().child(control)))
|
||||
.children(
|
||||
self.control
|
||||
.map(|control| div().flex_none().min_w_0().max_w_full().child(control)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -430,3 +430,51 @@ fn export_asks_the_owner(cx: &mut TestAppContext) {
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["export"]);
|
||||
}
|
||||
|
||||
fn bad_port(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let proxy = Proxy::Manual {
|
||||
host: "proxy.corp".into(),
|
||||
port: 0,
|
||||
bypass: Vec::new(),
|
||||
};
|
||||
ProxySettings::new("proxy", proxy)
|
||||
.on_apply(move |proxy, _, cx| say(&owner, format!("{proxy:?}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// With port 0 Apply rests, so a step back from nothing lands on the field before it.
|
||||
#[gpui::test]
|
||||
fn apply_rests_while_the_port_is_bad(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(bad_port, cx);
|
||||
cx.update(|window, _| {
|
||||
window.blur();
|
||||
window.focus_prev();
|
||||
});
|
||||
settle(cx);
|
||||
tap("space", cx);
|
||||
assert!(said(&host, cx).is_empty());
|
||||
}
|
||||
|
||||
fn default_size(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
FontSizeControl::new("size", 14.0, 14.0, (12.0, 24.0))
|
||||
.on_change(move |size, _, cx| say(&owner, format!("size {size}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// At the default size the way back rests, so the second stop wraps to the slider.
|
||||
#[gpui::test]
|
||||
fn the_way_back_rests_at_the_default_size(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(default_size, cx);
|
||||
tab(2, cx);
|
||||
tap("space", cx);
|
||||
assert!(said(&host, cx).is_empty());
|
||||
}
|
||||
|
||||
/// Stops: Do not disturb, then App, which is on.
|
||||
#[gpui::test]
|
||||
fn a_box_pressed_off_lets_its_pair_go(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(notices, cx);
|
||||
tab(2, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["[] false"]);
|
||||
}
|
||||
Reference in new issue
Block a user