Panics · P35 Settings: handlers in new; gone sections and themes mark none, drifted font sizes peg

This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 05:38:45 -04:00
1 parent 45cecebea5
commit ad472490e0
18 files changed
+384 -427

No files matched your search

+90 -77
View File
@@ -38,36 +38,36 @@ fn appearance(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
let [theme, accent, size, density, font] = [(); 5].map(|_| prefs.clone()); let [theme, accent, size, density, font] = [(); 5].map(|_| prefs.clone());
SettingsSection::new("Appearance") SettingsSection::new("Appearance")
.description("How the app looks.") .description("How the app looks.")
.row( .row(SettingsRow::new("Theme").control(ThemeSelector::new(
SettingsRow::new("Theme").control( "settings-theme",
ThemeSelector::new("settings-theme", now.appearance).on_change( now.appearance,
move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next), move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next),
), )))
),
)
.row( .row(
SettingsRow::new("Accent color") SettingsRow::new("Accent color")
.description("Buttons, links and what is chosen.") .description("Buttons, links and what is chosen.")
.control( .control(AccentColorPicker::new(
AccentColorPicker::new("settings-accent", now.accent, presets).on_change( "settings-accent",
move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next), 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))
.on_change(move |next, _, cx| change(&size, cx, |prefs| prefs.size = 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( .row(
SettingsRow::new("Density") SettingsRow::new("Density")
.description("How close things sit.") .description("How close things sit.")
.control( .control(DensitySelector::new(
DensitySelector::new("settings-density", now.density).on_change( "settings-density",
move |next, _, cx| change(&density, cx, |prefs| prefs.density = next), now.density,
), move |next, _, cx| change(&density, cx, |prefs| prefs.density = next),
), )),
) )
.row( .row(
SettingsRow::new("Font") SettingsRow::new("Font")
@@ -126,25 +126,32 @@ fn keyboard(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
fn network(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn network(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
let proxy = prefs.clone(); let proxy = prefs.clone();
SettingsSection::new("Network") SettingsSection::new("Network")
.row( .row(ProxySettings::new(
ProxySettings::new("settings-proxy", prefs.read(cx).proxy.clone()) "settings-proxy",
.on_apply(move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next)), prefs.read(cx).proxy.clone(),
) move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next),
))
.into_any_element() .into_any_element()
} }
fn privacy(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn privacy(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
let privacy = prefs.clone(); let privacy = prefs.clone();
PrivacySettings::new("settings-privacy", prefs.read(cx).privacy) PrivacySettings::new(
.on_change(move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next)) "settings-privacy",
.into_any_element() prefs.read(cx).privacy,
move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next),
)
.into_any_element()
} }
fn notifications(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn notifications(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
let notices = prefs.clone(); let notices = prefs.clone();
NotificationSettings::new("settings-notices", prefs.read(cx).notices.clone()) NotificationSettings::new(
.on_change(move |next, _, cx| change(&notices, cx, |prefs| prefs.notices = next)) "settings-notices",
.into_any_element() prefs.read(cx).notices.clone(),
move |next, _, cx| change(&notices, cx, |prefs| prefs.notices = next),
)
.into_any_element()
} }
fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
@@ -154,9 +161,13 @@ fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
Choice::new("home", "The home page"), Choice::new("home", "The home page"),
Choice::new("blank", "A blank window"), Choice::new("blank", "A blank window"),
]; ];
StartupSettings::new("settings-startup", prefs.read(cx).startup.clone(), pages) StartupSettings::new(
.on_change(move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next)) "settings-startup",
.into_any_element() prefs.read(cx).startup.clone(),
pages,
move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next),
)
.into_any_element()
} }
fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
@@ -175,37 +186,36 @@ fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
.child( .child(
SettingsSection::new("Storage") SettingsSection::new("Storage")
.description("What the app keeps on this device.") .description("What the app keeps on this device.")
.row( .row(StorageSettings::new(
StorageSettings::new("settings-storage", now.stores.clone()).on_clear( "settings-storage",
move |key, _, cx| { now.stores.clone(),
change(&cleared, cx, |prefs| { move |key, _, cx| {
prefs change(&cleared, cx, |prefs| {
.stores prefs
.iter_mut() .stores
.filter(|store| store.key == *key) .iter_mut()
.for_each(|store| store.bytes = 0) .filter(|store| store.key == *key)
}) .for_each(|store| store.bytes = 0)
}, })
), },
), )),
) )
.child( .child(
SettingsSection::new("Import and export").row( SettingsSection::new("Import and export").row(ImportExportSettings::new(
ImportExportSettings::new("settings-files") "settings-files",
.on_export(|_, _| log::info!("gallery: settings exported")) |_, _| log::info!("gallery: settings exported"),
.on_import(|path, _, _| { |path, _, _| log::info!("gallery: settings imported from {}", path.display()),
log::info!("gallery: settings imported from {}", path.display()) )),
}),
),
) )
.child( .child(
SettingsSection::new("Defaults") SettingsSection::new("Defaults")
.description("A larger font size shows here.") .description("A larger font size shows here.")
.row( .row(ResetToDefault::new(
ResetToDefault::new("settings-reset", changed) "settings-reset",
.on_reset(move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE)) changed,
.on_reset_all(move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE)), move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE),
), move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE),
)),
) )
.into_any_element() .into_any_element()
} }
@@ -220,22 +230,25 @@ fn advanced(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
.child( .child(
SettingsSection::new("Features") SettingsSection::new("Features")
.description("AdvancedSettings / FeatureFlags") .description("AdvancedSettings / FeatureFlags")
.row( .row(FeatureFlags::new(
FeatureFlags::new("settings-flags", now.flags.clone(), &fields.flags) "settings-flags",
.on_toggle(move |key, on, _, cx| { now.flags.clone(),
change(&toggled, cx, |prefs| { &fields.flags,
prefs move |key, on, _, cx| {
.flags change(&toggled, cx, |prefs| {
.iter_mut() prefs
.filter(|flag| flag.key == *key) .flags
.for_each(|flag| flag.on = on) .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)),
) )
.child(DeveloperModeToggle::new(
"settings-developer",
now.developer,
move |on, _, cx| change(&developer, cx, |prefs| prefs.developer = on),
))
.into_any_element() .into_any_element()
} }
+2 -3
View File
@@ -259,14 +259,13 @@ pub fn settings(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex() .flex()
.flex_col() .flex_col()
.gap_6() .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()), move |key, _, cx| change(&found, cx, |prefs| prefs.section = home(key).into()),
))) )))
.child(probe( .child(probe(
"settings-layout", "settings-layout",
div().w(px(880.)).child( div().w(px(880.)).child(
SettingsLayout::new("settings-layout", SECTIONS, selected) SettingsLayout::new("settings-layout", SECTIONS, selected, move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone()))
.on_select(move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone()))
.page(page), .page(page),
), ),
)), )),
+1 -1
View File
@@ -49,7 +49,7 @@ pub fn editor(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex() .flex()
.flex_col() .flex_col()
.gap_4() .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( .child(
div().flex().child( div().flex().child(
Button::new("theme-restore", "Back to Ely's theme") Button::new("theme-restore", "Back to Ely's theme")
+2 -2
View File
@@ -21,7 +21,7 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
specimens() specimens()
.child(specimen( .child(specimen(
"ThemeImporter", "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_palette(read.mode, Some(read.colors.clone()), cx);
Theme::set_mode(read.mode, 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( .child(specimen(
"SyntaxThemePicker", "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| { move |picked, _, cx| {
let theme = syntax_themes() let theme = syntax_themes()
.into_iter() .into_iter()
+12 -24
View File
@@ -40,7 +40,7 @@ pub struct FeatureFlags {
id: ElementId, id: ElementId,
flags: Vec<Flag>, flags: Vec<Flag>,
search: Entity<TextInput>, search: Entity<TextInput>,
on_toggle: Option<OnFlag>, on_toggle: OnFlag,
} }
impl FeatureFlags { impl FeatureFlags {
@@ -49,22 +49,15 @@ impl FeatureFlags {
id: impl Into<ElementId>, id: impl Into<ElementId>,
flags: impl IntoIterator<Item = Flag>, flags: impl IntoIterator<Item = Flag>,
search: &Entity<TextInput>, search: &Entity<TextInput>,
on_toggle: impl Fn(&SharedString, bool, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
flags: flags.into_iter().collect(), flags: flags.into_iter().collect(),
search: search.clone(), 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`. /// 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 { impl RenderOnce for FeatureFlags {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_toggle = self let on_toggle = self.on_toggle;
.on_toggle
.unwrap_or_else(|| panic!("feature flags {:?} has no on_toggle", self.id));
let query = self.search.read(cx).text().to_string(); let query = self.search.read(cx).text().to_string();
let theme = cx.theme(); let theme = cx.theme();
let rows = kept_flags(&self.flags, &query).into_iter().map(|flag| { let rows = kept_flags(&self.flags, &query).into_iter().map(|flag| {
@@ -134,29 +125,26 @@ type OnBool = Rc<dyn Fn(bool, &mut Window, &mut App)>;
pub struct DeveloperModeToggle { pub struct DeveloperModeToggle {
id: ElementId, id: ElementId,
on: bool, on: bool,
on_change: Option<OnBool>, on_change: OnBool,
} }
impl DeveloperModeToggle { impl DeveloperModeToggle {
pub fn new(id: impl Into<ElementId>, on: bool) -> Self { pub fn new(
id: impl Into<ElementId>,
on: bool,
on_change: impl Fn(bool, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
on, 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 { impl RenderOnce for DeveloperModeToggle {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("developer mode toggle {:?} has no on_change", self.id));
let on_change = on_change; let on_change = on_change;
div() div()
.flex() .flex()
+39 -54
View File
@@ -45,32 +45,26 @@ type OnAppearance = Rc<dyn Fn(Appearance, &mut Window, &mut App)>;
pub struct ThemeSelector { pub struct ThemeSelector {
id: ElementId, id: ElementId,
appearance: Appearance, appearance: Appearance,
on_change: Option<OnAppearance>, on_change: OnAppearance,
} }
impl ThemeSelector { impl ThemeSelector {
pub fn new(id: impl Into<ElementId>, appearance: Appearance) -> Self { pub fn new(
id: impl Into<ElementId>,
appearance: Appearance,
on_change: impl Fn(Appearance, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
appearance, 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 { impl RenderOnce for ThemeSelector {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("theme selector {:?} has no on_change", self.id));
let (_, words, _) = Appearance::ALL let (_, words, _) = Appearance::ALL
.iter() .iter()
.find(|(each, _, _)| *each == self.appearance) .find(|(each, _, _)| *each == self.appearance)
@@ -100,7 +94,7 @@ pub struct AccentColorPicker {
id: ElementId, id: ElementId,
color: Hsla, color: Hsla,
presets: Vec<(SharedString, Hsla)>, presets: Vec<(SharedString, Hsla)>,
on_change: Option<OnColor>, on_change: OnColor,
} }
impl AccentColorPicker { impl AccentColorPicker {
@@ -108,6 +102,7 @@ impl AccentColorPicker {
id: impl Into<ElementId>, id: impl Into<ElementId>,
color: impl Into<Hsla>, color: impl Into<Hsla>,
presets: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<Hsla>)>, presets: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<Hsla>)>,
on_change: impl Fn(Hsla, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
@@ -116,21 +111,14 @@ impl AccentColorPicker {
.into_iter() .into_iter()
.map(|(name, color)| (name.into(), color.into())) .map(|(name, color)| (name.into(), color.into()))
.collect(), .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 { impl RenderOnce for AccentColorPicker {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("accent color picker {:?} has no on_change", self.id));
let (picked, custom) = (on_change.clone(), on_change); let (picked, custom) = (on_change.clone(), on_change);
let report = |on_change: OnColor| { let report = |on_change: OnColor| {
move |color: Hsla, window: &mut Window, cx: &mut App| { move |color: Hsla, window: &mut Window, cx: &mut App| {
@@ -169,38 +157,38 @@ pub struct FontSizeControl {
size: f32, size: f32,
default: f32, default: f32,
range: (f32, f32), range: (f32, f32),
on_change: Option<OnSize>, on_change: OnSize,
} }
impl FontSizeControl { impl FontSizeControl {
/// `size` and `default` lie within `range`, in pixels. /// `size` and `default` lie within `range`, in pixels.
pub fn new(id: impl Into<ElementId>, size: f32, default: f32, range: (f32, f32)) -> Self { pub fn new(
assert!( id: impl Into<ElementId>,
range.0 < range.1 size: f32,
&& (range.0..=range.1).contains(&size) default: f32,
&& (range.0..=range.1).contains(&default), range: (f32, f32),
"font size {size} and default {default} outside {range:?}" 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 { Self {
id: id.into(), id: id.into(),
size, size: held("size", size),
default, default: held("default", default),
range, 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 { impl RenderOnce for FontSizeControl {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("font size control {:?} has no on_change", self.id));
let theme = cx.theme(); let theme = cx.theme();
let (slid, reset, default) = (on_change.clone(), on_change, self.default); let (slid, reset, default) = (on_change.clone(), on_change, self.default);
let (low, high) = self.range; let (low, high) = self.range;
@@ -258,7 +246,7 @@ type OnDensity = Rc<dyn Fn(Density, &mut Window, &mut App)>;
pub struct DensitySelector { pub struct DensitySelector {
id: ElementId, id: ElementId,
density: Density, density: Density,
on_change: Option<OnDensity>, on_change: OnDensity,
} }
const DENSITIES: [(Density, &str); 3] = [ const DENSITIES: [(Density, &str); 3] = [
@@ -268,25 +256,22 @@ const DENSITIES: [(Density, &str); 3] = [
]; ];
impl DensitySelector { impl DensitySelector {
pub fn new(id: impl Into<ElementId>, density: Density) -> Self { pub fn new(
id: impl Into<ElementId>,
density: Density,
on_change: impl Fn(Density, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
density, 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 { impl RenderOnce for DensitySelector {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("density selector {:?} has no on_change", self.id));
let (_, words) = DENSITIES let (_, words) = DENSITIES
.iter() .iter()
.find(|(each, _)| *each == self.density) .find(|(each, _)| *each == self.density)
+15 -18
View File
@@ -58,33 +58,27 @@ type OnDraft = Rc<dyn Fn(&ThemeDraft, &mut Window, &mut App)>;
pub struct ThemeEditor { pub struct ThemeEditor {
id: ElementId, id: ElementId,
draft: ThemeDraft, draft: ThemeDraft,
on_change: Option<OnDraft>, on_change: OnDraft,
} }
impl ThemeEditor { impl ThemeEditor {
pub fn new(id: impl Into<ElementId>, draft: ThemeDraft) -> Self { pub fn new(
id: impl Into<ElementId>,
draft: ThemeDraft,
on_change: impl Fn(&ThemeDraft, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
draft, 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 { impl RenderOnce for ThemeEditor {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("theme editor {id:?} has no on_change"));
let draft = Rc::new(self.draft); let draft = Rc::new(self.draft);
let edit = { let edit = {
let (draft, on_change) = (draft.clone(), on_change); let (draft, on_change) = (draft.clone(), on_change);
@@ -104,11 +98,14 @@ impl RenderOnce for ThemeEditor {
let look = SettingsSection::new("Look") let look = SettingsSection::new("Look")
.row(row( .row(row(
"Density", "Density",
DensitySelector::new((id.clone(), "density"), draft.density) DensitySelector::new(
.on_change(move |next, window, cx| { (id.clone(), "density"),
draft.density,
move |next, window, cx| {
density(&|d: &mut ThemeDraft| d.density = next, window, cx) density(&|d: &mut ThemeDraft| d.density = next, window, cx)
}) },
.into_any_element(), )
.into_any_element(),
)) ))
.row(row( .row(row(
"Corners", "Corners",
+7 -14
View File
@@ -46,32 +46,25 @@ pub(super) type OnTheme = Rc<dyn Fn(&VsCodeTheme, &mut Window, &mut App)>;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ThemeImporter { pub struct ThemeImporter {
id: ElementId, id: ElementId,
on_apply: Option<OnTheme>, on_apply: OnTheme,
} }
impl ThemeImporter { impl ThemeImporter {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(
id: impl Into<ElementId>,
on_apply: impl Fn(&VsCodeTheme, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), 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 { impl RenderOnce for ThemeImporter {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_apply = self let on_apply = self.on_apply;
.on_apply
.unwrap_or_else(|| panic!("theme importer {id:?} has no on_apply"));
let last = window.use_keyed_state((id.clone(), "read"), cx, |_, _| None::<Read>); let last = window.use_keyed_state((id.clone(), "read"), cx, |_, _| None::<Read>);
let took = last.clone(); let took = last.clone();
let zone = DropZone::new((id.clone(), "zone")) let zone = DropZone::new((id.clone(), "zone"))
+8 -11
View File
@@ -54,22 +54,21 @@ type OnProxy = Rc<dyn Fn(Proxy, &mut Window, &mut App)>;
pub struct ProxySettings { pub struct ProxySettings {
id: ElementId, id: ElementId,
proxy: Proxy, proxy: Proxy,
on_apply: Option<OnProxy>, on_apply: OnProxy,
} }
impl ProxySettings { impl ProxySettings {
pub fn new(id: impl Into<ElementId>, proxy: Proxy) -> Self { pub fn new(
id: impl Into<ElementId>,
proxy: Proxy,
on_apply: impl Fn(Proxy, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
proxy, 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 { impl RenderOnce for ProxySettings {
@@ -129,9 +128,7 @@ impl RenderOnce for ProxySettings {
"manual" => manual(&text(&host), &text(&port), &text(&bypass)), "manual" => manual(&text(&host), &text(&port), &text(&bypass)),
other => panic!("proxy settings: no way named {other}"), other => panic!("proxy settings: no way named {other}"),
}; };
let on_apply = self let on_apply = self.on_apply;
.on_apply
.unwrap_or_else(|| panic!("proxy settings {id:?} has no on_apply"));
let switched = fields.clone(); let switched = fields.clone();
div() div()
.flex() .flex()
+20 -32
View File
@@ -26,29 +26,26 @@ pub struct Privacy {
pub struct PrivacySettings { pub struct PrivacySettings {
id: ElementId, id: ElementId,
privacy: Privacy, privacy: Privacy,
on_change: Option<OnEdit<Privacy>>, on_change: OnEdit<Privacy>,
} }
impl PrivacySettings { impl PrivacySettings {
pub fn new(id: impl Into<ElementId>, privacy: Privacy) -> Self { pub fn new(
id: impl Into<ElementId>,
privacy: Privacy,
on_change: impl Fn(Privacy, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
privacy, 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 { impl RenderOnce for PrivacySettings {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.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 (id, privacy, on_change) = (self.id, self.privacy, &on_change);
let row = |key: &'static str, let row = |key: &'static str,
title: &'static str, title: &'static str,
@@ -108,29 +105,26 @@ pub struct Notices {
pub struct NotificationSettings { pub struct NotificationSettings {
id: ElementId, id: ElementId,
notices: Notices, notices: Notices,
on_change: Option<OnEdit<Notices>>, on_change: OnEdit<Notices>,
} }
impl NotificationSettings { impl NotificationSettings {
pub fn new(id: impl Into<ElementId>, notices: Notices) -> Self { pub fn new(
id: impl Into<ElementId>,
notices: Notices,
on_change: impl Fn(Notices, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
notices, 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 { impl RenderOnce for NotificationSettings {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.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 (id, notices, on_change) = (self.id, self.notices, &on_change);
let theme = cx.theme(); let theme = cx.theme();
let rows = notices.kinds.iter().map(|(kind, name)| { let rows = notices.kinds.iter().map(|(kind, name)| {
@@ -209,7 +203,7 @@ pub struct StartupSettings {
id: ElementId, id: ElementId,
startup: Startup, startup: Startup,
pages: Vec<Choice>, pages: Vec<Choice>,
on_change: Option<OnEdit<Startup>>, on_change: OnEdit<Startup>,
} }
impl StartupSettings { impl StartupSettings {
@@ -217,26 +211,20 @@ impl StartupSettings {
id: impl Into<ElementId>, id: impl Into<ElementId>,
startup: Startup, startup: Startup,
pages: impl IntoIterator<Item = Choice>, pages: impl IntoIterator<Item = Choice>,
on_change: impl Fn(Startup, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
startup, startup,
pages: pages.into_iter().collect(), 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 { impl RenderOnce for StartupSettings {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.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 (id, startup, on_change) = (self.id, self.startup, &on_change);
let switch = |key: &'static str, on: bool, set: fn(&mut Startup, bool)| { let switch = |key: &'static str, on: bool, set: fn(&mut Startup, bool)| {
Switch::new((id.clone(), key), on) Switch::new((id.clone(), key), on)
+4 -13
View File
@@ -46,7 +46,7 @@ pub struct SettingsSearch {
id: ElementId, id: ElementId,
entries: Vec<SettingEntry>, entries: Vec<SettingEntry>,
search: Entity<TextInput>, search: Entity<TextInput>,
on_pick: Option<OnKey>, on_pick: OnKey,
} }
impl SettingsSearch { impl SettingsSearch {
@@ -55,29 +55,20 @@ impl SettingsSearch {
id: impl Into<ElementId>, id: impl Into<ElementId>,
entries: impl IntoIterator<Item = SettingEntry>, entries: impl IntoIterator<Item = SettingEntry>,
search: &Entity<TextInput>, search: &Entity<TextInput>,
on_pick: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
entries: entries.into_iter().collect(), entries: entries.into_iter().collect(),
search: search.clone(), 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 { impl RenderOnce for SettingsSearch {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_pick = self let on_pick = self.on_pick;
.on_pick
.unwrap_or_else(|| panic!("settings search {:?} has no on_pick", self.id));
let query = self.search.read(cx).text().trim().to_string(); let query = self.search.read(cx).text().trim().to_string();
let theme = cx.theme(); let theme = cx.theme();
let found = if query.is_empty() { let found = if query.is_empty() {
+7 -17
View File
@@ -20,7 +20,7 @@ pub struct SettingsLayout {
sections: Vec<(SharedString, SharedString, IconName)>, sections: Vec<(SharedString, SharedString, IconName)>,
selected: SharedString, selected: SharedString,
page: Option<AnyElement>, page: Option<AnyElement>,
on_select: Option<OnKey>, on_select: OnKey,
} }
impl SettingsLayout { impl SettingsLayout {
@@ -29,22 +29,22 @@ impl SettingsLayout {
id: impl Into<ElementId>, id: impl Into<ElementId>,
sections: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>, IconName)>, sections: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>, IconName)>,
selected: impl Into<SharedString>, selected: impl Into<SharedString>,
on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let sections: Vec<_> = sections let sections: Vec<_> = sections
.into_iter() .into_iter()
.map(|(key, name, icon)| (key.into(), name.into(), icon)) .map(|(key, name, icon)| (key.into(), name.into(), icon))
.collect(); .collect();
let selected = selected.into(); let selected = selected.into();
assert!( if !sections.iter().any(|(key, _, _)| *key == selected) {
sections.iter().any(|(key, _, _)| *key == selected), log::error!("settings layout: no section {selected}; none open");
"settings layout: no section {selected}" }
);
Self { Self {
id: id.into(), id: id.into(),
sections, sections,
selected, selected,
page: None, 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.page = Some(page.into_any_element());
self 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 { impl RenderOnce for SettingsLayout {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_select = self let on_select = self.on_select;
.on_select
.unwrap_or_else(|| panic!("settings layout {:?} has no on_select", self.id));
let theme = cx.theme(); let theme = cx.theme();
let muted = theme.colors.fg_muted; let muted = theme.colors.fg_muted;
let list = self.sections.iter().fold( let list = self.sections.iter().fold(
+31 -59
View File
@@ -29,32 +29,26 @@ type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
pub struct StorageSettings { pub struct StorageSettings {
id: ElementId, id: ElementId,
stores: Vec<Store>, stores: Vec<Store>,
on_clear: Option<OnKey>, on_clear: OnKey,
} }
impl StorageSettings { impl StorageSettings {
pub fn new(id: impl Into<ElementId>, stores: impl IntoIterator<Item = Store>) -> Self { pub fn new(
id: impl Into<ElementId>,
stores: impl IntoIterator<Item = Store>,
on_clear: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
stores: stores.into_iter().collect(), 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 { impl RenderOnce for StorageSettings {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_clear = self let on_clear = self.on_clear;
.on_clear
.unwrap_or_else(|| panic!("storage settings {:?} has no on_clear", self.id));
let theme = cx.theme(); let theme = cx.theme();
let total: u64 = self.stores.iter().map(|store| store.bytes).sum(); let total: u64 = self.stores.iter().map(|store| store.bytes).sum();
let bar = self.stores.iter().fold( let bar = self.stores.iter().fold(
@@ -134,42 +128,30 @@ type Run = Rc<dyn Fn(&mut Window, &mut App)>;
pub struct ResetToDefault { pub struct ResetToDefault {
id: ElementId, id: ElementId,
changed: Vec<Changed>, changed: Vec<Changed>,
on_reset: Option<OnKey>, on_reset: OnKey,
on_reset_all: Option<Run>, on_reset_all: Run,
} }
impl ResetToDefault { impl ResetToDefault {
pub fn new(id: impl Into<ElementId>, changed: impl IntoIterator<Item = Changed>) -> Self { pub fn new(
id: impl Into<ElementId>,
changed: impl IntoIterator<Item = Changed>,
on_reset: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
on_reset_all: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
changed: changed.into_iter().collect(), changed: changed.into_iter().collect(),
on_reset: None, on_reset: Rc::new(on_reset),
on_reset_all: None, 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 { impl RenderOnce for ResetToDefault {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let on_reset = self let on_reset = self.on_reset;
.on_reset let on_reset_all = self.on_reset_all;
.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 theme = cx.theme(); let theme = cx.theme();
if self.changed.is_empty() { if self.changed.is_empty() {
return div() return div()
@@ -249,38 +231,28 @@ type OnFile = Rc<dyn Fn(PathBuf, &mut Window, &mut App)>;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ImportExportSettings { pub struct ImportExportSettings {
id: ElementId, id: ElementId,
on_export: Option<Run>, on_export: Run,
on_import: Option<OnFile>, on_import: OnFile,
} }
impl ImportExportSettings { impl ImportExportSettings {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(
id: impl Into<ElementId>,
on_export: impl Fn(&mut Window, &mut App) + 'static,
on_import: impl Fn(PathBuf, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
on_export: None, on_export: Rc::new(on_export),
on_import: None, 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 { impl RenderOnce for ImportExportSettings {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_export = self let on_export = self.on_export;
.on_export let on_import = self.on_import;
.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, on_import) = (on_export, on_import); let (on_export, on_import) = (on_export, on_import);
div() div()
.flex() .flex()
+7 -17
View File
@@ -57,7 +57,7 @@ pub struct SyntaxThemePicker {
id: ElementId, id: ElementId,
themes: Vec<SyntaxTheme>, themes: Vec<SyntaxTheme>,
selected: SharedString, selected: SharedString,
on_change: Option<OnName>, on_change: OnName,
} }
impl SyntaxThemePicker { impl SyntaxThemePicker {
@@ -66,36 +66,26 @@ impl SyntaxThemePicker {
id: impl Into<ElementId>, id: impl Into<ElementId>,
themes: impl IntoIterator<Item = SyntaxTheme>, themes: impl IntoIterator<Item = SyntaxTheme>,
selected: impl Into<SharedString>, selected: impl Into<SharedString>,
on_change: impl Fn(&'static str, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let themes: Vec<SyntaxTheme> = themes.into_iter().collect(); let themes: Vec<SyntaxTheme> = themes.into_iter().collect();
let selected = selected.into(); let selected = selected.into();
assert!( if !themes.iter().any(|theme| theme.name == selected.as_ref()) {
themes.iter().any(|theme| theme.name == selected.as_ref()), log::error!("syntax theme picker: no theme {selected}; none chosen");
"syntax theme picker: no theme {selected}" }
);
Self { Self {
id: id.into(), id: id.into(),
themes, themes,
selected, 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 { impl RenderOnce for SyntaxThemePicker {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("syntax theme picker {id:?} has no on_change"));
let mode: Mode = cx.theme().mode(); let mode: Mode = cx.theme().mode();
let cards = self.themes.iter().map(|theme| { let cards = self.themes.iter().map(|theme| {
let (pick, name) = (on_change.clone(), theme.name); let (pick, name) = (on_change.clone(), theme.name);
+49 -7
View File
@@ -8,9 +8,13 @@ use crate::settings::{AccentColorPicker, FontSizeControl};
fn accent(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement { fn accent(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
let (red, blue) = (hsla(0.0, 0.6, 0.5, 1.0), hsla(0.6, 0.6, 0.5, 1.0)); 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)]) AccentColorPicker::new(
.on_change(move |color, _, cx| say(&owner, format!("blue {}", color == blue), cx)) "accent",
.into_any_element() 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. /// 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<Desk>) -> AnyElement { fn default_size(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
FontSizeControl::new("size", 14.0, 14.0, (12.0, 24.0)) FontSizeControl::new("size", 14.0, 14.0, (12.0, 24.0), move |size, _, cx| {
.on_change(move |size, _, cx| say(&owner, format!("size {size}"), cx)) say(&owner, format!("size {size}"), cx)
.into_any_element() })
.into_any_element()
} }
/// At the default size the way back rests, so the second stop wraps to the slider. /// 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<Desk>) -> AnyElement {
.collect(); .collect();
div() div()
.w(px(280.0)) .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() .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:?}" "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<Desk>) -> 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);
}
+60 -47
View File
@@ -96,8 +96,8 @@ fn search(window: &mut Window, cx: &mut App, owner: Entity<Desk>) -> AnyElement
entry("size", "Font size", &["zoom"]), entry("size", "Font size", &["zoom"]),
], ],
&field, &field,
move |key, _, cx| say(&owner, format!("go {key}"), cx),
) )
.on_pick(move |key, _, cx| say(&owner, format!("go {key}"), cx))
.into_any_element() .into_any_element()
} }
@@ -121,10 +121,13 @@ fn reset(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
default: "off".into(), default: "off".into(),
}; };
let (one, all) = (owner.clone(), owner); let (one, all) = (owner.clone(), owner);
ResetToDefault::new("reset", [changed("a"), changed("b")]) ResetToDefault::new(
.on_reset(move |key, _, cx| say(&one, format!("reset {key}"), cx)) "reset",
.on_reset_all(move |_, cx| say(&all, "reset all".into(), cx)) [changed("a"), changed("b")],
.into_any_element() 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. /// 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<Desk>) -> AnyElement {
stage: Stage::Beta, stage: Stage::Beta,
on, on,
}; };
FeatureFlags::new("flags", [flag("tabs", false), flag("sync", true)], &field) FeatureFlags::new(
.on_toggle(move |key, on, _, cx| say(&owner, format!("{key} {on}"), cx)) "flags",
.into_any_element() [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. /// Stops: the search, then each flag's switch.
@@ -170,11 +177,10 @@ fn notices(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
on: vec![("mention".into(), "App".into())], on: vec![("mention".into(), "App".into())],
quiet: false, quiet: false,
}; };
NotificationSettings::new("notices", notices) NotificationSettings::new("notices", notices, move |notices, _, cx| {
.on_change(move |notices, _, cx| { say(&owner, format!("{:?} {}", notices.on, notices.quiet), cx)
say(&owner, format!("{:?} {}", notices.on, notices.quiet), cx) })
}) .into_any_element()
.into_any_element()
} }
/// Stops: Do not disturb, then a box per channel. /// Stops: Do not disturb, then a box per channel.
@@ -211,8 +217,8 @@ fn storage(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
clearable: false, 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() .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<Desk>) -> AnyElement { fn size(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
FontSizeControl::new("size", 18.0, 14.0, (12.0, 24.0)) FontSizeControl::new("size", 18.0, 14.0, (12.0, 24.0), move |size, _, cx| {
.on_change(move |size, _, cx| say(&owner, format!("size {size}"), cx)) say(&owner, format!("size {size}"), cx)
.into_any_element() })
.into_any_element()
} }
/// Stops: the slider, then the way back. /// 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<Desk>) -> AnyElement { fn theme(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
ThemeSelector::new("theme", Appearance::Light) ThemeSelector::new("theme", Appearance::Light, move |appearance, _, cx| {
.on_change(move |appearance, _, cx| say(&owner, format!("{appearance:?}"), cx)) say(&owner, format!("{appearance:?}"), cx)
.into_any_element() })
.into_any_element()
} }
/// Stops: a segment each. /// Stops: a segment each.
@@ -262,9 +270,10 @@ fn proxy(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
port: 8080, port: 8080,
bypass: vec!["localhost".into()], bypass: vec!["localhost".into()],
}; };
ProxySettings::new("proxy", proxy) ProxySettings::new("proxy", proxy, move |proxy, _, cx| {
.on_apply(move |proxy, _, cx| say(&owner, format!("{proxy:?}"), cx)) say(&owner, format!("{proxy:?}"), cx)
.into_any_element() })
.into_any_element()
} }
/// Apply is the last stop, so a step back from nothing reaches it. /// 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<Desk>) -> AnyElement {
("b", "Network", IconName::Globe), ("b", "Network", IconName::Globe),
], ],
"a", "a",
move |key, _, cx| say(&owner, format!("open {key}"), cx),
) )
.on_select(move |key, _, cx| say(&owner, format!("open {key}"), cx))
.page(div()) .page(div())
.into_any_element() .into_any_element()
} }
@@ -313,9 +322,10 @@ fn privacy(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
personalization: false, personalization: false,
history: true, history: true,
}; };
PrivacySettings::new("privacy", privacy) PrivacySettings::new("privacy", privacy, move |next, _, cx| {
.on_change(move |next, _, cx| say(&owner, format!("{next:?}"), cx)) say(&owner, format!("{next:?}"), cx)
.into_any_element() })
.into_any_element()
} }
/// Stops: a switch per field, telemetry first. /// Stops: a switch per field, telemetry first.
@@ -339,15 +349,14 @@ fn quiet(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
on: vec![("replies".into(), "App".into())], on: vec![("replies".into(), "App".into())],
quiet: false, quiet: false,
}; };
NotificationSettings::new("notices", notices) NotificationSettings::new("notices", notices, move |next, _, cx| {
.on_change(move |next, _, cx| { say(
say( &owner,
&owner, format!("quiet {} on {}", next.quiet, next.on.len()),
format!("quiet {} on {}", next.quiet, next.on.len()), cx,
cx, )
) })
}) .into_any_element()
.into_any_element()
} }
/// Stops: Do not disturb, then a box per kind and channel. /// Stops: Do not disturb, then a box per kind and channel.
@@ -370,8 +379,8 @@ fn startup(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
"startup", "startup",
startup, startup,
[Choice::new("last", "Where I left off")], [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() .into_any_element()
} }
@@ -385,9 +394,10 @@ fn open_at_login_turns_on(cx: &mut TestAppContext) {
} }
fn developer(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement { fn developer(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
DeveloperModeToggle::new("developer", false) DeveloperModeToggle::new("developer", false, move |on, _, cx| {
.on_change(move |on, _, cx| say(&owner, format!("developer {on}"), cx)) say(&owner, format!("developer {on}"), cx)
.into_any_element() })
.into_any_element()
} }
/// Stops: the switch. /// Stops: the switch.
@@ -400,10 +410,12 @@ fn developer_mode_turns_on(cx: &mut TestAppContext) {
} }
fn files(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement { fn files(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
ImportExportSettings::new("files") ImportExportSettings::new(
.on_export(move |_, cx| say(&owner, "export".into(), cx)) "files",
.on_import(|_, _, _| {}) move |_, cx| say(&owner, "export".into(), cx),
.into_any_element() |_, _, _| {},
)
.into_any_element()
} }
/// Stops: Export first. /// Stops: Export first.
@@ -421,9 +433,10 @@ fn bad_port(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
port: 0, port: 0,
bypass: Vec::new(), bypass: Vec::new(),
}; };
ProxySettings::new("proxy", proxy) ProxySettings::new("proxy", proxy, move |proxy, _, cx| {
.on_apply(move |proxy, _, cx| say(&owner, format!("{proxy:?}"), cx)) say(&owner, format!("{proxy:?}"), cx)
.into_any_element() })
.into_any_element()
} }
/// With port 0 Apply rests, so a step back from nothing lands on the field before it. /// With port 0 Apply rests, so a step back from nothing lands on the field before it.
+28 -30
View File
@@ -14,18 +14,17 @@ use crate::{
}; };
fn editor(_: &mut Window, cx: &mut App, owner: Entity<Desk>) -> AnyElement { fn editor(_: &mut Window, cx: &mut App, owner: Entity<Desk>) -> AnyElement {
ThemeEditor::new("editor", ThemeDraft::of(cx.theme())) ThemeEditor::new("editor", ThemeDraft::of(cx.theme()), move |draft, _, cx| {
.on_change(move |draft, _, cx| { say(
say( &owner,
&owner, format!(
format!( "{:?} contrast {} still {}",
"{:?} contrast {} still {}", draft.density, draft.high_contrast, draft.reduced_motion
draft.density, draft.high_contrast, draft.reduced_motion ),
), cx,
cx, )
) })
}) .into_any_element()
.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. /// 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<Desk>) -> AnyElement { fn colors(_: &mut Window, cx: &mut App, owner: Entity<Desk>) -> AnyElement {
ThemeEditor::new("editor", ThemeDraft::of(cx.theme())) ThemeEditor::new("editor", ThemeDraft::of(cx.theme()), move |draft, _, cx| {
.on_change(move |draft, _, cx| { let background = gpui::Rgba::from(draft.colors.bg);
let background = gpui::Rgba::from(draft.colors.bg); say(&owner, crate::forms::hex(background), cx)
say(&owner, crate::forms::hex(background), cx) })
}) .into_any_element()
.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. /// 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<Desk>) -> AnyElement { fn look(_: &mut Window, cx: &mut App, owner: Entity<Desk>) -> AnyElement {
ThemeEditor::new("editor", ThemeDraft::of(cx.theme())) ThemeEditor::new("editor", ThemeDraft::of(cx.theme()), move |draft, _, cx| {
.on_change(move |draft, _, cx| { let words = format!(
let words = format!( "{:?} corners {} text {}",
"{:?} corners {} text {}", draft.density, draft.radius_scale, draft.font_scale
draft.density, draft.radius_scale, draft.font_scale );
); say(&owner, words, cx)
say(&owner, words, cx) })
}) .into_any_element()
.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. /// 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<Desk>) -> AnyElement { fn syntax(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
SyntaxThemePicker::new("syntax", syntax_themes(), "Ely") SyntaxThemePicker::new("syntax", syntax_themes(), "Ely", move |name, _, cx| {
.on_change(move |name, _, cx| say(&owner, name.to_string(), cx)) say(&owner, name.to_string(), cx)
.into_any_element() })
.into_any_element()
} }
/// Stops: a card for each code palette; Space on the third picks Paper. /// Stops: a card for each code palette; Space on the third picks Paper.
+2 -1
View File
@@ -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] P32 Canvas & Design — `src/canvas`
- [x] P33 DB & Dev Tools — `src/devtools` - [x] P33 DB & Dev Tools — `src/devtools`
- [x] P34 Dashboard — `src/dashboard` - [x] P34 Dashboard — `src/dashboard`
- [ ] P35 Settings — `src/settings` - [x] P35 Settings — `src/settings`
- [ ] P36 Account — `src/account` - [ ] P36 Account — `src/account`
- [ ] P37 Onboarding — `src/onboarding` - [ ] P37 Onboarding — `src/onboarding`
- [ ] P38 Interaction — `src/interaction` - [ ] 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. - 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. - 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. - 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.