diff --git a/examples/gallery/pages/settings/pages.rs b/examples/gallery/pages/settings/pages.rs index 0caacef..e56ee88 100644 --- a/examples/gallery/pages/settings/pages.rs +++ b/examples/gallery/pages/settings/pages.rs @@ -38,36 +38,36 @@ fn appearance(prefs: &Entity, fields: &Fields, cx: &App) -> AnyElement { let [theme, accent, size, density, font] = [(); 5].map(|_| prefs.clone()); SettingsSection::new("Appearance") .description("How the app looks.") - .row( - SettingsRow::new("Theme").control( - ThemeSelector::new("settings-theme", now.appearance).on_change( - move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next), - ), - ), - ) + .row(SettingsRow::new("Theme").control(ThemeSelector::new( + "settings-theme", + now.appearance, + move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next), + ))) .row( SettingsRow::new("Accent color") .description("Buttons, links and what is chosen.") - .control( - AccentColorPicker::new("settings-accent", now.accent, presets).on_change( - move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next), - ), - ), - ) - .row( - SettingsRow::new("Font size").control( - FontSizeControl::new("settings-size", now.size, SIZE, (11.0, 20.0)) - .on_change(move |next, _, cx| change(&size, cx, |prefs| prefs.size = next)), - ), + .control(AccentColorPicker::new( + "settings-accent", + now.accent, + presets, + move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next), + )), ) + .row(SettingsRow::new("Font size").control(FontSizeControl::new( + "settings-size", + now.size, + SIZE, + (11.0, 20.0), + move |next, _, cx| change(&size, cx, |prefs| prefs.size = next), + ))) .row( SettingsRow::new("Density") .description("How close things sit.") - .control( - DensitySelector::new("settings-density", now.density).on_change( - move |next, _, cx| change(&density, cx, |prefs| prefs.density = next), - ), - ), + .control(DensitySelector::new( + "settings-density", + now.density, + move |next, _, cx| change(&density, cx, |prefs| prefs.density = next), + )), ) .row( SettingsRow::new("Font") @@ -126,25 +126,32 @@ fn keyboard(prefs: &Entity, fields: &Fields, cx: &App) -> AnyElement { fn network(prefs: &Entity, cx: &App) -> AnyElement { let proxy = prefs.clone(); SettingsSection::new("Network") - .row( - ProxySettings::new("settings-proxy", prefs.read(cx).proxy.clone()) - .on_apply(move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next)), - ) + .row(ProxySettings::new( + "settings-proxy", + prefs.read(cx).proxy.clone(), + move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next), + )) .into_any_element() } fn privacy(prefs: &Entity, cx: &App) -> AnyElement { let privacy = prefs.clone(); - PrivacySettings::new("settings-privacy", prefs.read(cx).privacy) - .on_change(move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next)) - .into_any_element() + PrivacySettings::new( + "settings-privacy", + prefs.read(cx).privacy, + move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next), + ) + .into_any_element() } fn notifications(prefs: &Entity, cx: &App) -> AnyElement { let notices = prefs.clone(); - NotificationSettings::new("settings-notices", prefs.read(cx).notices.clone()) - .on_change(move |next, _, cx| change(¬ices, cx, |prefs| prefs.notices = next)) - .into_any_element() + NotificationSettings::new( + "settings-notices", + prefs.read(cx).notices.clone(), + move |next, _, cx| change(¬ices, cx, |prefs| prefs.notices = next), + ) + .into_any_element() } fn startup(prefs: &Entity, cx: &App) -> AnyElement { @@ -154,9 +161,13 @@ fn startup(prefs: &Entity, cx: &App) -> AnyElement { Choice::new("home", "The home page"), Choice::new("blank", "A blank window"), ]; - StartupSettings::new("settings-startup", prefs.read(cx).startup.clone(), pages) - .on_change(move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next)) - .into_any_element() + StartupSettings::new( + "settings-startup", + prefs.read(cx).startup.clone(), + pages, + move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next), + ) + .into_any_element() } fn storage(prefs: &Entity, cx: &App) -> AnyElement { @@ -175,37 +186,36 @@ fn storage(prefs: &Entity, cx: &App) -> AnyElement { .child( SettingsSection::new("Storage") .description("What the app keeps on this device.") - .row( - StorageSettings::new("settings-storage", now.stores.clone()).on_clear( - move |key, _, cx| { - change(&cleared, cx, |prefs| { - prefs - .stores - .iter_mut() - .filter(|store| store.key == *key) - .for_each(|store| store.bytes = 0) - }) - }, - ), - ), + .row(StorageSettings::new( + "settings-storage", + now.stores.clone(), + move |key, _, cx| { + change(&cleared, cx, |prefs| { + prefs + .stores + .iter_mut() + .filter(|store| store.key == *key) + .for_each(|store| store.bytes = 0) + }) + }, + )), ) .child( - SettingsSection::new("Import and export").row( - ImportExportSettings::new("settings-files") - .on_export(|_, _| log::info!("gallery: settings exported")) - .on_import(|path, _, _| { - log::info!("gallery: settings imported from {}", path.display()) - }), - ), + SettingsSection::new("Import and export").row(ImportExportSettings::new( + "settings-files", + |_, _| log::info!("gallery: settings exported"), + |path, _, _| log::info!("gallery: settings imported from {}", path.display()), + )), ) .child( SettingsSection::new("Defaults") .description("A larger font size shows here.") - .row( - ResetToDefault::new("settings-reset", changed) - .on_reset(move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE)) - .on_reset_all(move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE)), - ), + .row(ResetToDefault::new( + "settings-reset", + changed, + move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE), + move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE), + )), ) .into_any_element() } @@ -220,22 +230,25 @@ fn advanced(prefs: &Entity, fields: &Fields, cx: &App) -> AnyElement { .child( SettingsSection::new("Features") .description("AdvancedSettings / FeatureFlags") - .row( - FeatureFlags::new("settings-flags", now.flags.clone(), &fields.flags) - .on_toggle(move |key, on, _, cx| { - change(&toggled, cx, |prefs| { - prefs - .flags - .iter_mut() - .filter(|flag| flag.key == *key) - .for_each(|flag| flag.on = on) - }) - }), - ), - ) - .child( - DeveloperModeToggle::new("settings-developer", now.developer) - .on_change(move |on, _, cx| change(&developer, cx, |prefs| prefs.developer = on)), + .row(FeatureFlags::new( + "settings-flags", + now.flags.clone(), + &fields.flags, + move |key, on, _, cx| { + change(&toggled, cx, |prefs| { + prefs + .flags + .iter_mut() + .filter(|flag| flag.key == *key) + .for_each(|flag| flag.on = on) + }) + }, + )), ) + .child(DeveloperModeToggle::new( + "settings-developer", + now.developer, + move |on, _, cx| change(&developer, cx, |prefs| prefs.developer = on), + )) .into_any_element() } diff --git a/examples/gallery/pages/settings/window.rs b/examples/gallery/pages/settings/window.rs index 97e198d..55d7917 100644 --- a/examples/gallery/pages/settings/window.rs +++ b/examples/gallery/pages/settings/window.rs @@ -259,14 +259,13 @@ pub fn settings(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { .flex() .flex_col() .gap_6() - .child(div().w(px(360.)).child(SettingsSearch::new("settings-search", entries, &search).on_pick( + .child(div().w(px(360.)).child(SettingsSearch::new("settings-search", entries, &search, move |key, _, cx| change(&found, cx, |prefs| prefs.section = home(key).into()), ))) .child(probe( "settings-layout", div().w(px(880.)).child( - SettingsLayout::new("settings-layout", SECTIONS, selected) - .on_select(move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone())) + SettingsLayout::new("settings-layout", SECTIONS, selected, move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone())) .page(page), ), )), diff --git a/examples/gallery/pages/theme/editing.rs b/examples/gallery/pages/theme/editing.rs index eec9a2f..85017da 100644 --- a/examples/gallery/pages/theme/editing.rs +++ b/examples/gallery/pages/theme/editing.rs @@ -49,7 +49,7 @@ pub fn editor(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> { .flex() .flex_col() .gap_4() - .child(ThemeEditor::new("theme-editor", draft).on_change(|draft, _, cx| apply(draft, cx))) + .child(ThemeEditor::new("theme-editor", draft, |draft, _, cx| apply(draft, cx))) .child( div().flex().child( Button::new("theme-restore", "Back to Ely's theme") diff --git a/examples/gallery/pages/theme/importing.rs b/examples/gallery/pages/theme/importing.rs index 33da772..ae4c00a 100644 --- a/examples/gallery/pages/theme/importing.rs +++ b/examples/gallery/pages/theme/importing.rs @@ -21,7 +21,7 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> specimens() .child(specimen( "ThemeImporter", - div().w(px(360.)).child(ThemeImporter::new("theme-importer").on_apply(|read, _, cx| { + div().w(px(360.)).child(ThemeImporter::new("theme-importer", |read, _, cx| { Theme::set_palette(read.mode, Some(read.colors.clone()), cx); Theme::set_mode(read.mode, cx); })), @@ -29,7 +29,7 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> )) .child(specimen( "SyntaxThemePicker", - div().w(px(360.)).child(SyntaxThemePicker::new("theme-syntax", syntax_themes(), name).on_change( + div().w(px(360.)).child(SyntaxThemePicker::new("theme-syntax", syntax_themes(), name, move |picked, _, cx| { let theme = syntax_themes() .into_iter() diff --git a/src/settings/advanced.rs b/src/settings/advanced.rs index a642304..e2ef329 100644 --- a/src/settings/advanced.rs +++ b/src/settings/advanced.rs @@ -40,7 +40,7 @@ pub struct FeatureFlags { id: ElementId, flags: Vec, search: Entity, - on_toggle: Option, + on_toggle: OnFlag, } impl FeatureFlags { @@ -49,22 +49,15 @@ impl FeatureFlags { id: impl Into, flags: impl IntoIterator, search: &Entity, + on_toggle: impl Fn(&SharedString, bool, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), flags: flags.into_iter().collect(), search: search.clone(), - on_toggle: None, + on_toggle: Rc::new(on_toggle), } } - - pub fn on_toggle( - mut self, - handler: impl Fn(&SharedString, bool, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_toggle = Some(Rc::new(handler)); - self - } } /// The flags whose name or description holds every word of `query`. @@ -83,9 +76,7 @@ fn kept_flags<'a>(flags: &'a [Flag], query: &str) -> Vec<&'a Flag> { impl RenderOnce for FeatureFlags { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_toggle = self - .on_toggle - .unwrap_or_else(|| panic!("feature flags {:?} has no on_toggle", self.id)); + let on_toggle = self.on_toggle; let query = self.search.read(cx).text().to_string(); let theme = cx.theme(); let rows = kept_flags(&self.flags, &query).into_iter().map(|flag| { @@ -134,29 +125,26 @@ type OnBool = Rc; pub struct DeveloperModeToggle { id: ElementId, on: bool, - on_change: Option, + on_change: OnBool, } impl DeveloperModeToggle { - pub fn new(id: impl Into, on: bool) -> Self { + pub fn new( + id: impl Into, + on: bool, + on_change: impl Fn(bool, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), on, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for DeveloperModeToggle { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("developer mode toggle {:?} has no on_change", self.id)); + let on_change = self.on_change; let on_change = on_change; div() .flex() diff --git a/src/settings/appearance.rs b/src/settings/appearance.rs index 0a6f398..286abe7 100644 --- a/src/settings/appearance.rs +++ b/src/settings/appearance.rs @@ -45,32 +45,26 @@ type OnAppearance = Rc; pub struct ThemeSelector { id: ElementId, appearance: Appearance, - on_change: Option, + on_change: OnAppearance, } impl ThemeSelector { - pub fn new(id: impl Into, appearance: Appearance) -> Self { + pub fn new( + id: impl Into, + appearance: Appearance, + on_change: impl Fn(Appearance, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), appearance, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change( - mut self, - handler: impl Fn(Appearance, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for ThemeSelector { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("theme selector {:?} has no on_change", self.id)); + let on_change = self.on_change; let (_, words, _) = Appearance::ALL .iter() .find(|(each, _, _)| *each == self.appearance) @@ -100,7 +94,7 @@ pub struct AccentColorPicker { id: ElementId, color: Hsla, presets: Vec<(SharedString, Hsla)>, - on_change: Option, + on_change: OnColor, } impl AccentColorPicker { @@ -108,6 +102,7 @@ impl AccentColorPicker { id: impl Into, color: impl Into, presets: impl IntoIterator, impl Into)>, + on_change: impl Fn(Hsla, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), @@ -116,21 +111,14 @@ impl AccentColorPicker { .into_iter() .map(|(name, color)| (name.into(), color.into())) .collect(), - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change(mut self, handler: impl Fn(Hsla, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for AccentColorPicker { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("accent color picker {:?} has no on_change", self.id)); + let on_change = self.on_change; let (picked, custom) = (on_change.clone(), on_change); let report = |on_change: OnColor| { move |color: Hsla, window: &mut Window, cx: &mut App| { @@ -169,38 +157,38 @@ pub struct FontSizeControl { size: f32, default: f32, range: (f32, f32), - on_change: Option, + on_change: OnSize, } impl FontSizeControl { /// `size` and `default` lie within `range`, in pixels. - pub fn new(id: impl Into, size: f32, default: f32, range: (f32, f32)) -> Self { - assert!( - range.0 < range.1 - && (range.0..=range.1).contains(&size) - && (range.0..=range.1).contains(&default), - "font size {size} and default {default} outside {range:?}" - ); + pub fn new( + id: impl Into, + size: f32, + default: f32, + range: (f32, f32), + on_change: impl Fn(f32, &mut Window, &mut App) + 'static, + ) -> Self { + assert!(range.0 < range.1, "font sizes {range:?} run backwards"); + let held = |what: &str, value: f32| { + if !(range.0..=range.1).contains(&value) { + log::error!("font size control: {what} {value} outside {range:?}; pegged"); + } + value.clamp(range.0, range.1) + }; Self { id: id.into(), - size, - default, + size: held("size", size), + default: held("default", default), range, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change(mut self, handler: impl Fn(f32, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for FontSizeControl { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("font size control {:?} has no on_change", self.id)); + let on_change = self.on_change; let theme = cx.theme(); let (slid, reset, default) = (on_change.clone(), on_change, self.default); let (low, high) = self.range; @@ -258,7 +246,7 @@ type OnDensity = Rc; pub struct DensitySelector { id: ElementId, density: Density, - on_change: Option, + on_change: OnDensity, } const DENSITIES: [(Density, &str); 3] = [ @@ -268,25 +256,22 @@ const DENSITIES: [(Density, &str); 3] = [ ]; impl DensitySelector { - pub fn new(id: impl Into, density: Density) -> Self { + pub fn new( + id: impl Into, + density: Density, + on_change: impl Fn(Density, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), density, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change(mut self, handler: impl Fn(Density, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for DensitySelector { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("density selector {:?} has no on_change", self.id)); + let on_change = self.on_change; let (_, words) = DENSITIES .iter() .find(|(each, _)| *each == self.density) diff --git a/src/settings/editor.rs b/src/settings/editor.rs index 3caef7f..ff072bf 100644 --- a/src/settings/editor.rs +++ b/src/settings/editor.rs @@ -58,33 +58,27 @@ type OnDraft = Rc; pub struct ThemeEditor { id: ElementId, draft: ThemeDraft, - on_change: Option, + on_change: OnDraft, } impl ThemeEditor { - pub fn new(id: impl Into, draft: ThemeDraft) -> Self { + pub fn new( + id: impl Into, + draft: ThemeDraft, + on_change: impl Fn(&ThemeDraft, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), draft, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change( - mut self, - handler: impl Fn(&ThemeDraft, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for ThemeEditor { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let id = self.id; - let on_change = self - .on_change - .unwrap_or_else(|| panic!("theme editor {id:?} has no on_change")); + let on_change = self.on_change; let draft = Rc::new(self.draft); let edit = { let (draft, on_change) = (draft.clone(), on_change); @@ -104,11 +98,14 @@ impl RenderOnce for ThemeEditor { let look = SettingsSection::new("Look") .row(row( "Density", - DensitySelector::new((id.clone(), "density"), draft.density) - .on_change(move |next, window, cx| { + DensitySelector::new( + (id.clone(), "density"), + draft.density, + move |next, window, cx| { density(&|d: &mut ThemeDraft| d.density = next, window, cx) - }) - .into_any_element(), + }, + ) + .into_any_element(), )) .row(row( "Corners", diff --git a/src/settings/importer.rs b/src/settings/importer.rs index 3149cca..6b4c5a6 100644 --- a/src/settings/importer.rs +++ b/src/settings/importer.rs @@ -46,32 +46,25 @@ pub(super) type OnTheme = Rc; #[derive(IntoElement)] pub struct ThemeImporter { id: ElementId, - on_apply: Option, + on_apply: OnTheme, } impl ThemeImporter { - pub fn new(id: impl Into) -> Self { + pub fn new( + id: impl Into, + on_apply: impl Fn(&VsCodeTheme, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), - on_apply: None, + on_apply: Rc::new(on_apply), } } - - pub fn on_apply( - mut self, - handler: impl Fn(&VsCodeTheme, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_apply = Some(Rc::new(handler)); - self - } } impl RenderOnce for ThemeImporter { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let id = self.id; - let on_apply = self - .on_apply - .unwrap_or_else(|| panic!("theme importer {id:?} has no on_apply")); + let on_apply = self.on_apply; let last = window.use_keyed_state((id.clone(), "read"), cx, |_, _| None::); let took = last.clone(); let zone = DropZone::new((id.clone(), "zone")) diff --git a/src/settings/network.rs b/src/settings/network.rs index 1950656..ca00811 100644 --- a/src/settings/network.rs +++ b/src/settings/network.rs @@ -54,22 +54,21 @@ type OnProxy = Rc; pub struct ProxySettings { id: ElementId, proxy: Proxy, - on_apply: Option, + on_apply: OnProxy, } impl ProxySettings { - pub fn new(id: impl Into, proxy: Proxy) -> Self { + pub fn new( + id: impl Into, + proxy: Proxy, + on_apply: impl Fn(Proxy, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), proxy, - on_apply: None, + on_apply: Rc::new(on_apply), } } - - pub fn on_apply(mut self, handler: impl Fn(Proxy, &mut Window, &mut App) + 'static) -> Self { - self.on_apply = Some(Rc::new(handler)); - self - } } impl RenderOnce for ProxySettings { @@ -129,9 +128,7 @@ impl RenderOnce for ProxySettings { "manual" => manual(&text(&host), &text(&port), &text(&bypass)), other => panic!("proxy settings: no way named {other}"), }; - let on_apply = self - .on_apply - .unwrap_or_else(|| panic!("proxy settings {id:?} has no on_apply")); + let on_apply = self.on_apply; let switched = fields.clone(); div() .flex() diff --git a/src/settings/preferences.rs b/src/settings/preferences.rs index ba95b10..4029cf4 100644 --- a/src/settings/preferences.rs +++ b/src/settings/preferences.rs @@ -26,29 +26,26 @@ pub struct Privacy { pub struct PrivacySettings { id: ElementId, privacy: Privacy, - on_change: Option>, + on_change: OnEdit, } impl PrivacySettings { - pub fn new(id: impl Into, privacy: Privacy) -> Self { + pub fn new( + id: impl Into, + privacy: Privacy, + on_change: impl Fn(Privacy, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), privacy, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change(mut self, handler: impl Fn(Privacy, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for PrivacySettings { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("privacy settings {:?} has no on_change", self.id)); + let on_change = self.on_change; let (id, privacy, on_change) = (self.id, self.privacy, &on_change); let row = |key: &'static str, title: &'static str, @@ -108,29 +105,26 @@ pub struct Notices { pub struct NotificationSettings { id: ElementId, notices: Notices, - on_change: Option>, + on_change: OnEdit, } impl NotificationSettings { - pub fn new(id: impl Into, notices: Notices) -> Self { + pub fn new( + id: impl Into, + notices: Notices, + on_change: impl Fn(Notices, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), notices, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change(mut self, handler: impl Fn(Notices, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for NotificationSettings { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("notification settings {:?} has no on_change", self.id)); + let on_change = self.on_change; let (id, notices, on_change) = (self.id, self.notices, &on_change); let theme = cx.theme(); let rows = notices.kinds.iter().map(|(kind, name)| { @@ -209,7 +203,7 @@ pub struct StartupSettings { id: ElementId, startup: Startup, pages: Vec, - on_change: Option>, + on_change: OnEdit, } impl StartupSettings { @@ -217,26 +211,20 @@ impl StartupSettings { id: impl Into, startup: Startup, pages: impl IntoIterator, + on_change: impl Fn(Startup, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), startup, pages: pages.into_iter().collect(), - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change(mut self, handler: impl Fn(Startup, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for StartupSettings { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_change = self - .on_change - .unwrap_or_else(|| panic!("startup settings {:?} has no on_change", self.id)); + let on_change = self.on_change; 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) diff --git a/src/settings/search.rs b/src/settings/search.rs index faf4a70..e31bde9 100644 --- a/src/settings/search.rs +++ b/src/settings/search.rs @@ -46,7 +46,7 @@ pub struct SettingsSearch { id: ElementId, entries: Vec, search: Entity, - on_pick: Option, + on_pick: OnKey, } impl SettingsSearch { @@ -55,29 +55,20 @@ impl SettingsSearch { id: impl Into, entries: impl IntoIterator, search: &Entity, + on_pick: impl Fn(&SharedString, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), entries: entries.into_iter().collect(), search: search.clone(), - on_pick: None, + on_pick: Rc::new(on_pick), } } - - pub fn on_pick( - mut self, - handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_pick = Some(Rc::new(handler)); - self - } } impl RenderOnce for SettingsSearch { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_pick = self - .on_pick - .unwrap_or_else(|| panic!("settings search {:?} has no on_pick", self.id)); + let on_pick = self.on_pick; let query = self.search.read(cx).text().trim().to_string(); let theme = cx.theme(); let found = if query.is_empty() { diff --git a/src/settings/section.rs b/src/settings/section.rs index dd004b9..b356a65 100644 --- a/src/settings/section.rs +++ b/src/settings/section.rs @@ -20,7 +20,7 @@ pub struct SettingsLayout { sections: Vec<(SharedString, SharedString, IconName)>, selected: SharedString, page: Option, - on_select: Option, + on_select: OnKey, } impl SettingsLayout { @@ -29,22 +29,22 @@ impl SettingsLayout { id: impl Into, sections: impl IntoIterator, impl Into, IconName)>, selected: impl Into, + on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static, ) -> Self { let sections: Vec<_> = sections .into_iter() .map(|(key, name, icon)| (key.into(), name.into(), icon)) .collect(); let selected = selected.into(); - assert!( - sections.iter().any(|(key, _, _)| *key == selected), - "settings layout: no section {selected}" - ); + if !sections.iter().any(|(key, _, _)| *key == selected) { + log::error!("settings layout: no section {selected}; none open"); + } Self { id: id.into(), sections, selected, page: None, - on_select: None, + on_select: Rc::new(on_select), } } @@ -53,21 +53,11 @@ impl SettingsLayout { self.page = Some(page.into_any_element()); self } - - pub fn on_select( - mut self, - handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_select = Some(Rc::new(handler)); - self - } } impl RenderOnce for SettingsLayout { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_select = self - .on_select - .unwrap_or_else(|| panic!("settings layout {:?} has no on_select", self.id)); + let on_select = self.on_select; let theme = cx.theme(); let muted = theme.colors.fg_muted; let list = self.sections.iter().fold( diff --git a/src/settings/storage.rs b/src/settings/storage.rs index 9ecb913..524ba46 100644 --- a/src/settings/storage.rs +++ b/src/settings/storage.rs @@ -29,32 +29,26 @@ type OnKey = Rc; pub struct StorageSettings { id: ElementId, stores: Vec, - on_clear: Option, + on_clear: OnKey, } impl StorageSettings { - pub fn new(id: impl Into, stores: impl IntoIterator) -> Self { + pub fn new( + id: impl Into, + stores: impl IntoIterator, + on_clear: impl Fn(&SharedString, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), stores: stores.into_iter().collect(), - on_clear: None, + on_clear: Rc::new(on_clear), } } - - pub fn on_clear( - mut self, - handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_clear = Some(Rc::new(handler)); - self - } } impl RenderOnce for StorageSettings { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_clear = self - .on_clear - .unwrap_or_else(|| panic!("storage settings {:?} has no on_clear", self.id)); + let on_clear = self.on_clear; let theme = cx.theme(); let total: u64 = self.stores.iter().map(|store| store.bytes).sum(); let bar = self.stores.iter().fold( @@ -134,42 +128,30 @@ type Run = Rc; pub struct ResetToDefault { id: ElementId, changed: Vec, - on_reset: Option, - on_reset_all: Option, + on_reset: OnKey, + on_reset_all: Run, } impl ResetToDefault { - pub fn new(id: impl Into, changed: impl IntoIterator) -> Self { + pub fn new( + id: impl Into, + changed: impl IntoIterator, + on_reset: impl Fn(&SharedString, &mut Window, &mut App) + 'static, + on_reset_all: impl Fn(&mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), changed: changed.into_iter().collect(), - on_reset: None, - on_reset_all: None, + on_reset: Rc::new(on_reset), + on_reset_all: Rc::new(on_reset_all), } } - - pub fn on_reset( - mut self, - handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_reset = Some(Rc::new(handler)); - self - } - - pub fn on_reset_all(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { - self.on_reset_all = Some(Rc::new(handler)); - self - } } impl RenderOnce for ResetToDefault { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let on_reset = self - .on_reset - .unwrap_or_else(|| panic!("reset to default {:?} has no on_reset", self.id)); - let on_reset_all = self - .on_reset_all - .unwrap_or_else(|| panic!("reset to default {:?} has no on_reset_all", self.id)); + let on_reset = self.on_reset; + let on_reset_all = self.on_reset_all; let theme = cx.theme(); if self.changed.is_empty() { return div() @@ -249,38 +231,28 @@ type OnFile = Rc; #[derive(IntoElement)] pub struct ImportExportSettings { id: ElementId, - on_export: Option, - on_import: Option, + on_export: Run, + on_import: OnFile, } impl ImportExportSettings { - pub fn new(id: impl Into) -> Self { + pub fn new( + id: impl Into, + on_export: impl Fn(&mut Window, &mut App) + 'static, + on_import: impl Fn(PathBuf, &mut Window, &mut App) + 'static, + ) -> Self { Self { id: id.into(), - on_export: None, - on_import: None, + on_export: Rc::new(on_export), + on_import: Rc::new(on_import), } } - - pub fn on_export(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { - self.on_export = Some(Rc::new(handler)); - self - } - - pub fn on_import(mut self, handler: impl Fn(PathBuf, &mut Window, &mut App) + 'static) -> Self { - self.on_import = Some(Rc::new(handler)); - self - } } impl RenderOnce for ImportExportSettings { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - let on_export = self - .on_export - .unwrap_or_else(|| panic!("import export settings {:?} has no on_export", self.id)); - let on_import = self - .on_import - .unwrap_or_else(|| panic!("import export settings {:?} has no on_import", self.id)); + let on_export = self.on_export; + let on_import = self.on_import; let (on_export, on_import) = (on_export, on_import); div() .flex() diff --git a/src/settings/syntax.rs b/src/settings/syntax.rs index 40d170d..0abc524 100644 --- a/src/settings/syntax.rs +++ b/src/settings/syntax.rs @@ -57,7 +57,7 @@ pub struct SyntaxThemePicker { id: ElementId, themes: Vec, selected: SharedString, - on_change: Option, + on_change: OnName, } impl SyntaxThemePicker { @@ -66,36 +66,26 @@ impl SyntaxThemePicker { id: impl Into, themes: impl IntoIterator, selected: impl Into, + on_change: impl Fn(&'static str, &mut Window, &mut App) + 'static, ) -> Self { let themes: Vec = themes.into_iter().collect(); let selected = selected.into(); - assert!( - themes.iter().any(|theme| theme.name == selected.as_ref()), - "syntax theme picker: no theme {selected}" - ); + if !themes.iter().any(|theme| theme.name == selected.as_ref()) { + log::error!("syntax theme picker: no theme {selected}; none chosen"); + } Self { id: id.into(), themes, selected, - on_change: None, + on_change: Rc::new(on_change), } } - - pub fn on_change( - mut self, - handler: impl Fn(&'static str, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_change = Some(Rc::new(handler)); - self - } } impl RenderOnce for SyntaxThemePicker { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let id = self.id; - let on_change = self - .on_change - .unwrap_or_else(|| panic!("syntax theme picker {id:?} has no on_change")); + let on_change = self.on_change; let mode: Mode = cx.theme().mode(); let cards = self.themes.iter().map(|theme| { let (pick, name) = (on_change.clone(), theme.name); diff --git a/src/settings/tests/appearance.rs b/src/settings/tests/appearance.rs index f8998a8..0d0b10d 100644 --- a/src/settings/tests/appearance.rs +++ b/src/settings/tests/appearance.rs @@ -8,9 +8,13 @@ use crate::settings::{AccentColorPicker, FontSizeControl}; fn accent(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { let (red, blue) = (hsla(0.0, 0.6, 0.5, 1.0), hsla(0.6, 0.6, 0.5, 1.0)); - AccentColorPicker::new("accent", red, [("Red", red), ("Blue", blue)]) - .on_change(move |color, _, cx| say(&owner, format!("blue {}", color == blue), cx)) - .into_any_element() + AccentColorPicker::new( + "accent", + red, + [("Red", red), ("Blue", blue)], + move |color, _, cx| say(&owner, format!("blue {}", color == blue), cx), + ) + .into_any_element() } /// Stops: the presets, Red then Blue, then the custom well. @@ -23,9 +27,10 @@ fn a_preset_sets_the_accent(cx: &mut TestAppContext) { } fn default_size(_: &mut Window, _: &mut App, owner: Entity) -> 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() + FontSizeControl::new("size", 14.0, 14.0, (12.0, 24.0), 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. @@ -43,7 +48,12 @@ fn many_presets(_: &mut Window, _: &mut App, _: Entity) -> AnyElement { .collect(); div() .w(px(280.0)) - .child(AccentColorPicker::new("accent", presets[0].1, presets).on_change(|_, _, _| {})) + .child(AccentColorPicker::new( + "accent", + presets[0].1, + presets, + |_, _, _| {}, + )) .into_any_element() } @@ -61,3 +71,35 @@ fn many_presets_fold_inside_a_narrow_box(cx: &mut TestAppContext) { "its swatches fold to a second row: {picker:?}" ); } + +/// A size saved past the range, a section and a code palette no longer offered. +fn drifted(_: &mut Window, _: &mut App, _: Entity) -> AnyElement { + let sections = [("a", "Appearance", crate::primitives::IconName::Palette)]; + div() + .w(px(480.0)) + .child(FontSizeControl::new( + "size", + 40.0, + 9.0, + (12.0, 24.0), + |_, _, _| {}, + )) + .child(crate::settings::SettingsLayout::new( + "layout", + sections, + "gone", + |_, _, _| {}, + )) + .child(crate::settings::SyntaxThemePicker::new( + "syntax", + crate::theme::syntax_themes(), + "Gone", + |_, _, _| {}, + )) + .into_any_element() +} + +#[gpui::test] +fn settings_that_drifted_draw(cx: &mut TestAppContext) { + let _ = desk(drifted, cx); +} diff --git a/src/settings/tests/mod.rs b/src/settings/tests/mod.rs index 954ed04..43a1c4d 100644 --- a/src/settings/tests/mod.rs +++ b/src/settings/tests/mod.rs @@ -96,8 +96,8 @@ fn search(window: &mut Window, cx: &mut App, owner: Entity) -> AnyElement entry("size", "Font size", &["zoom"]), ], &field, + move |key, _, cx| say(&owner, format!("go {key}"), cx), ) - .on_pick(move |key, _, cx| say(&owner, format!("go {key}"), cx)) .into_any_element() } @@ -121,10 +121,13 @@ fn reset(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { default: "off".into(), }; let (one, all) = (owner.clone(), owner); - ResetToDefault::new("reset", [changed("a"), changed("b")]) - .on_reset(move |key, _, cx| say(&one, format!("reset {key}"), cx)) - .on_reset_all(move |_, cx| say(&all, "reset all".into(), cx)) - .into_any_element() + ResetToDefault::new( + "reset", + [changed("a"), changed("b")], + move |key, _, cx| say(&one, format!("reset {key}"), cx), + move |_, cx| say(&all, "reset all".into(), cx), + ) + .into_any_element() } /// Stops: each row's way back, then Reset all, which asks twice. @@ -149,9 +152,13 @@ fn flags(window: &mut Window, cx: &mut App, owner: Entity) -> AnyElement { stage: Stage::Beta, on, }; - FeatureFlags::new("flags", [flag("tabs", false), flag("sync", true)], &field) - .on_toggle(move |key, on, _, cx| say(&owner, format!("{key} {on}"), cx)) - .into_any_element() + FeatureFlags::new( + "flags", + [flag("tabs", false), flag("sync", true)], + &field, + move |key, on, _, cx| say(&owner, format!("{key} {on}"), cx), + ) + .into_any_element() } /// Stops: the search, then each flag's switch. @@ -170,11 +177,10 @@ fn notices(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { on: vec![("mention".into(), "App".into())], quiet: false, }; - NotificationSettings::new("notices", notices) - .on_change(move |notices, _, cx| { - say(&owner, format!("{:?} {}", notices.on, notices.quiet), cx) - }) - .into_any_element() + NotificationSettings::new("notices", notices, move |notices, _, cx| { + say(&owner, format!("{:?} {}", notices.on, notices.quiet), cx) + }) + .into_any_element() } /// Stops: Do not disturb, then a box per channel. @@ -211,8 +217,8 @@ fn storage(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { clearable: false, }, ], + move |key, _, cx| say(&owner, format!("clear {key}"), cx), ) - .on_clear(move |key, _, cx| say(&owner, format!("clear {key}"), cx)) .into_any_element() } @@ -227,9 +233,10 @@ fn a_store_clears_on_the_second_press(cx: &mut TestAppContext) { } fn size(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { - FontSizeControl::new("size", 18.0, 14.0, (12.0, 24.0)) - .on_change(move |size, _, cx| say(&owner, format!("size {size}"), cx)) - .into_any_element() + FontSizeControl::new("size", 18.0, 14.0, (12.0, 24.0), move |size, _, cx| { + say(&owner, format!("size {size}"), cx) + }) + .into_any_element() } /// Stops: the slider, then the way back. @@ -242,9 +249,10 @@ fn the_way_back_gives_the_default_size(cx: &mut TestAppContext) { } fn theme(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { - ThemeSelector::new("theme", Appearance::Light) - .on_change(move |appearance, _, cx| say(&owner, format!("{appearance:?}"), cx)) - .into_any_element() + ThemeSelector::new("theme", Appearance::Light, move |appearance, _, cx| { + say(&owner, format!("{appearance:?}"), cx) + }) + .into_any_element() } /// Stops: a segment each. @@ -262,9 +270,10 @@ fn proxy(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { port: 8080, bypass: vec!["localhost".into()], }; - ProxySettings::new("proxy", proxy) - .on_apply(move |proxy, _, cx| say(&owner, format!("{proxy:?}"), cx)) - .into_any_element() + ProxySettings::new("proxy", proxy, move |proxy, _, cx| { + say(&owner, format!("{proxy:?}"), cx) + }) + .into_any_element() } /// Apply is the last stop, so a step back from nothing reaches it. @@ -291,8 +300,8 @@ fn layout(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { ("b", "Network", IconName::Globe), ], "a", + move |key, _, cx| say(&owner, format!("open {key}"), cx), ) - .on_select(move |key, _, cx| say(&owner, format!("open {key}"), cx)) .page(div()) .into_any_element() } @@ -313,9 +322,10 @@ fn privacy(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { personalization: false, history: true, }; - PrivacySettings::new("privacy", privacy) - .on_change(move |next, _, cx| say(&owner, format!("{next:?}"), cx)) - .into_any_element() + PrivacySettings::new("privacy", privacy, move |next, _, cx| { + say(&owner, format!("{next:?}"), cx) + }) + .into_any_element() } /// Stops: a switch per field, telemetry first. @@ -339,15 +349,14 @@ fn quiet(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { on: vec![("replies".into(), "App".into())], quiet: false, }; - NotificationSettings::new("notices", notices) - .on_change(move |next, _, cx| { - say( - &owner, - format!("quiet {} on {}", next.quiet, next.on.len()), - cx, - ) - }) - .into_any_element() + NotificationSettings::new("notices", notices, move |next, _, cx| { + say( + &owner, + format!("quiet {} on {}", next.quiet, next.on.len()), + cx, + ) + }) + .into_any_element() } /// Stops: Do not disturb, then a box per kind and channel. @@ -370,8 +379,8 @@ fn startup(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { "startup", startup, [Choice::new("last", "Where I left off")], + move |next, _, cx| say(&owner, format!("login {}", next.at_login), cx), ) - .on_change(move |next, _, cx| say(&owner, format!("login {}", next.at_login), cx)) .into_any_element() } @@ -385,9 +394,10 @@ fn open_at_login_turns_on(cx: &mut TestAppContext) { } fn developer(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { - DeveloperModeToggle::new("developer", false) - .on_change(move |on, _, cx| say(&owner, format!("developer {on}"), cx)) - .into_any_element() + DeveloperModeToggle::new("developer", false, move |on, _, cx| { + say(&owner, format!("developer {on}"), cx) + }) + .into_any_element() } /// Stops: the switch. @@ -400,10 +410,12 @@ fn developer_mode_turns_on(cx: &mut TestAppContext) { } fn files(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { - ImportExportSettings::new("files") - .on_export(move |_, cx| say(&owner, "export".into(), cx)) - .on_import(|_, _, _| {}) - .into_any_element() + ImportExportSettings::new( + "files", + move |_, cx| say(&owner, "export".into(), cx), + |_, _, _| {}, + ) + .into_any_element() } /// Stops: Export first. @@ -421,9 +433,10 @@ fn bad_port(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { port: 0, bypass: Vec::new(), }; - ProxySettings::new("proxy", proxy) - .on_apply(move |proxy, _, cx| say(&owner, format!("{proxy:?}"), cx)) - .into_any_element() + ProxySettings::new("proxy", proxy, 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. diff --git a/src/settings/tests/theme.rs b/src/settings/tests/theme.rs index 54f2b9f..66113d2 100644 --- a/src/settings/tests/theme.rs +++ b/src/settings/tests/theme.rs @@ -14,18 +14,17 @@ use crate::{ }; fn editor(_: &mut Window, cx: &mut App, owner: Entity) -> AnyElement { - ThemeEditor::new("editor", ThemeDraft::of(cx.theme())) - .on_change(move |draft, _, cx| { - say( - &owner, - format!( - "{:?} contrast {} still {}", - draft.density, draft.high_contrast, draft.reduced_motion - ), - cx, - ) - }) - .into_any_element() + ThemeEditor::new("editor", ThemeDraft::of(cx.theme()), move |draft, _, cx| { + say( + &owner, + format!( + "{:?} contrast {} still {}", + draft.density, draft.high_contrast, draft.reduced_motion + ), + cx, + ) + }) + .into_any_element() } /// Stops: the three densities, corners, text size, high contrast, reduced motion, then the colors. A switch hands on a draft with its own flag turned and the rest as it stood. @@ -47,12 +46,11 @@ fn a_switch_turns_its_own_flag(cx: &mut TestAppContext) { } fn colors(_: &mut Window, cx: &mut App, owner: Entity) -> AnyElement { - ThemeEditor::new("editor", ThemeDraft::of(cx.theme())) - .on_change(move |draft, _, cx| { - let background = gpui::Rgba::from(draft.colors.bg); - say(&owner, crate::forms::hex(background), cx) - }) - .into_any_element() + ThemeEditor::new("editor", ThemeDraft::of(cx.theme()), move |draft, _, cx| { + let background = gpui::Rgba::from(draft.colors.bg); + say(&owner, crate::forms::hex(background), cx) + }) + .into_any_element() } /// The first color is the eighth stop; Enter opens its picker, where the hex field follows the eyedropper, a stop on macOS alone, and takes a new background. @@ -71,15 +69,14 @@ fn a_color_well_sets_its_own_color(cx: &mut TestAppContext) { } fn look(_: &mut Window, cx: &mut App, owner: Entity) -> AnyElement { - ThemeEditor::new("editor", ThemeDraft::of(cx.theme())) - .on_change(move |draft, _, cx| { - let words = format!( - "{:?} corners {} text {}", - draft.density, draft.radius_scale, draft.font_scale - ); - say(&owner, words, cx) - }) - .into_any_element() + ThemeEditor::new("editor", ThemeDraft::of(cx.theme()), move |draft, _, cx| { + let words = format!( + "{:?} corners {} text {}", + draft.density, draft.radius_scale, draft.font_scale + ); + say(&owner, words, cx) + }) + .into_any_element() } /// Stops: Compact, Standard, Comfortable, corners, text size. Compact picks the density alone, and Right on each slider moves its own measure a step. @@ -157,9 +154,10 @@ fn the_thumb_covers_the_chosen_segment(cx: &mut TestAppContext) { } fn syntax(_: &mut Window, _: &mut App, owner: Entity) -> AnyElement { - SyntaxThemePicker::new("syntax", syntax_themes(), "Ely") - .on_change(move |name, _, cx| say(&owner, name.to_string(), cx)) - .into_any_element() + SyntaxThemePicker::new("syntax", syntax_themes(), "Ely", move |name, _, cx| { + say(&owner, name.to_string(), cx) + }) + .into_any_element() } /// Stops: a card for each code palette; Space on the third picks Paper. diff --git a/tasks/panics.md b/tasks/panics.md index a67291f..b700d91 100644 --- a/tasks/panics.md +++ b/tasks/panics.md @@ -49,7 +49,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca - [x] P32 Canvas & Design — `src/canvas` - [x] P33 DB & Dev Tools — `src/devtools` - [x] P34 Dashboard — `src/dashboard` -- [ ] P35 Settings — `src/settings` +- [x] P35 Settings — `src/settings` - [ ] P36 Account — `src/account` - [ ] P37 Onboarding — `src/onboarding` - [ ] P38 Interaction — `src/interaction` @@ -100,3 +100,4 @@ One line per item: what changed, what stayed and why, review rounds. - P32: the eight design panels take their handlers in `new` (AGENTS.md says so); edges, links, wires, links in progress, resizes, restacks, topics and whiteboard edits that name a node, port, shape, layer or topic gone from this frame log and change nothing; a wire's release fits its source as it now stands; `Topic::adding`, `removing` and `renaming` return `Option`; a mind map's selection, a palette's tool and a history position no longer held mark none. Kept: one-value contracts (alignment counts, frames, zoom, polygon sides, brush hues and type weights among the theme's tables), lookups over the panels' own option tables, button-gated expects. Review: 2 rounds. - P33: six devtools take their handlers in `new` (contrast checker, environment selector, GraphQL explorer, WebSocket console, structure editor, index manager); `collection::moved` returns `Option` for a gone key or target or a drop into a request; a gone environment shows none chosen; an ER table without a place stays out. Kept: readings, costs, lookups over the components' own tables, button-gated expects, JSON that read writes back. Review: 1 round. - P34: the refresh interval selector, filter bar and dashboard grid take their handlers in `new`; `tiles::arranged` and `stepped` return `Option` for a tile gone from the grid, so a landing or step after the owner removed it moves nothing; tiles without cards and cards without tiles stay out. Kept: columns, cells, intervals offered, services and uptimes, lookups from one render. Review: 1 round. +- P35: eighteen settings components take their handlers in `new`; a section or syntax theme not offered marks none; a font size or default outside its range pegs. Kept: options offered by construction, the proxy's Apply behind its read, drops that bring a file. Review: 1 round.