authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 02:08:14-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 07:14:47-07:00
log514ad509ad5650d2b51457a47964e4f5e02846c8
tree9d70996fb5e66fcbf4f52998734e61cf177469fb
parentc7f6712ad38d29d83cef963b14d7a473da6546e3
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: Options picks with segmented controls and menus, says what a choice does, and shows only what applies here

- Page tabs and Notebooks pick Left or Right, and Appearance System, Light or Dark, with segmented controls - Lock when idle for picks Never or a time from one menu, and Default font's size sits beside its font - Every control lines up in one column, with a line under those whose label can't say what they do - Options leaves out the cache's folder and Check for updates automatically in the browser - OK keeps the other choices where the user name is left blank Assisted-by: claude-opus-5.5

1 files changed, 340 insertions(+), 302 deletions(-)

crates/snowbound/src/options.rs+340-302
......@@ -3,7 +3,7 @@
33//! chosen.
44
55use crate::{
6 State, UserEvent, platform,
6 State, UserEvent,
77 settings::{Backend, ColorScheme},
88 update,
99};
......@@ -12,15 +12,14 @@ use canvas::editor::DefaultFont;
1212use ui::{Anchor, Axis, Extent, Flags, Id, Size, Spec, Theme, Ui, fill, popup::Item, px};
1313use winit::keyboard::NamedKey;
1414
15const WIDTH: f32 = 680.0;
15const WIDTH: f32 = 700.0;
1616const NAV: f32 = 150.0;
17/// The column the fields' labels share.
18const LABEL: f32 = 104.0;
1917const SCHEMES: [(ColorScheme, &str); 3] = [
2018 (ColorScheme::System, "System"),
2119 (ColorScheme::Light, "Light"),
2220 (ColorScheme::Dark, "Dark"),
2321];
22const SIDES: [&str; 2] = ["Left", "Right"];
2423/// The sizes Default font offers, OneNote 2010's list in points.
2524const SIZES: [f32; 19] = [
2625 8.0, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0,
......@@ -42,16 +41,35 @@ struct Group {
4241
4342struct Row {
4443 label: &'static str,
44 /// A line under the control for what the label can't say; empty for none.
45 hint: &'static str,
4546 /// Other words search finds the row by.
4647 keywords: &'static str,
4748 control: Control,
4849}
4950
51impl Row {
52 const fn new(label: &'static str, keywords: &'static str, control: Control) -> Self {
53 Self {
54 label,
55 hint: "",
56 keywords,
57 control,
58 }
59 }
60
61 const fn hint(self, hint: &'static str) -> Self {
62 Self { hint, ..self }
63 }
64}
65
66/// Every control stands in one column, after the labels' column.
5067enum Control {
51 /// A check box before the label, bound to a choice.
68 /// A check box labelled by the row, bound to a choice.
5269 Check(fn(&mut Options) -> &mut bool),
53 /// The label in the labels' column, then what the function builds.
54 Field(fn(&mut State, &mut Options)),
70 /// The row's label in the labels' column, then what the function builds, given the
71 /// label to name its control by.
72 Field(fn(&mut State, &mut Options, &str)),
5573 /// What `build` makes across the row, unlabelled, from the search's words; `finds` tells
5674 /// whether they find anything in it. Words found in the row's own texts give it none.
5775 Block {
......@@ -60,33 +78,33 @@ enum Control {
6078 },
6179}
6280
63/// Every section, in the list's order.
81/// Every section, in the list's order, with only the rows that apply here.
6482const SECTIONS: &[Section] = &[
6583 Section {
6684 name: "General",
6785 groups: &[
6886 Group {
69 heading: "User Interface Options",
87 heading: "Interface",
7088 rows: &[
71 Row {
72 label: "Appearance:",
73 keywords: "color colour scheme dark light mode system theme",
74 control: Control::Field(appearance),
75 },
76 Row {
77 label: "Pages match UI theme",
78 keywords: "dark white paper background",
79 control: Control::Check(|options| &mut options.pages_match),
80 },
89 Row::new(
90 "Appearance:",
91 "color colour scheme dark light mode system theme",
92 Control::Field(appearance),
93 ),
94 Row::new(
95 "Pages match the appearance",
96 "dark white paper background ui theme",
97 Control::Check(|options| &mut options.pages_match),
98 )
99 .hint("Turned off, pages stay white"),
81100 ],
82101 },
83102 Group {
84103 heading: "Personalize",
85 rows: &[Row {
86 label: "User name:",
87 keywords: "author name",
88 control: Control::Field(user_name_field),
89 }],
104 rows: &[
105 Row::new("User name:", "author", Control::Field(user_name_field))
106 .hint("Shown with your edits in shared notebooks"),
107 ],
90108 },
91109 ],
92110 },
......@@ -96,76 +114,68 @@ const SECTIONS: &[Section] = &[
96114 Group {
97115 heading: "Default font",
98116 rows: &[
99 Row {
100 label: "Font:",
101 keywords: "typeface text new",
102 control: Control::Field(font_face),
103 },
104 Row {
105 label: "Size:",
106 keywords: "font points text new",
107 control: Control::Field(font_size),
108 },
109 Row {
110 label: "Font color:",
111 keywords: "colour text new automatic",
112 control: Control::Field(font_color),
113 },
117 Row::new(
118 "Font:",
119 "typeface size points text new",
120 Control::Field(font_face),
121 ),
122 Row::new(
123 "Color:",
124 "font colour text new automatic",
125 Control::Field(font_color),
126 ),
114127 ],
115128 },
116129 Group {
117 heading: "Default style theme",
118 rows: &[Row {
119 label: "New notebooks:",
120 keywords: "default for new notebooks styles headings modern",
121 control: Control::Field(notebook_theme),
122 }],
130 heading: "Font styles",
131 rows: &[Row::new(
132 "New notebooks:",
133 "default style theme headings modern",
134 Control::Field(notebook_theme),
135 )
136 .hint("The styles a new notebook starts with")],
123137 },
124138 Group {
125139 heading: "Proofing",
126 rows: &[Row {
127 label: "Hide spelling errors",
128 keywords: "spell check misspelled words",
129 control: Control::Check(|options| &mut options.hide_spelling),
130 }],
140 rows: &[Row::new(
141 "Hide spelling errors",
142 "spell check misspelled words",
143 Control::Check(|options| &mut options.hide_spelling),
144 )],
131145 },
132146 Group {
133147 heading: "AutoFormat",
134 rows: &[Row {
135 label: "Markdown shortcuts",
136 keywords: "markdown autoformat heading bullet numbering list to do quote \
137 bold italic strikethrough code typing",
138 control: Control::Check(|options| &mut options.markdown),
139 }],
148 rows: &[Row::new(
149 "Markdown shortcuts",
150 "markdown autoformat heading bullet numbering list to do quote bold \
151 italic strikethrough code typing",
152 Control::Check(|options| &mut options.markdown),
153 )
154 .hint("Type # for a heading, - for a bullet, **bold** and more")],
140155 },
141156 Group {
142157 heading: "Pen",
143 rows: &[Row {
144 label: "Use pen pressure sensitivity",
145 keywords: "tablet stylus ink stroke width drawing",
146 control: Control::Check(|options| &mut options.pen_pressure),
147 }],
158 rows: &[Row::new(
159 "Use pen pressure sensitivity",
160 "tablet stylus ink stroke width drawing",
161 Control::Check(|options| &mut options.pen_pressure),
162 )],
148163 },
149164 Group {
150165 heading: "Passwords",
151166 rows: &[
152 Row {
153 label: "Lock password protected sections after I have not worked in \
154 them for:",
155 keywords: "protect protected lock unlock idle minutes timeout security",
156 control: Control::Check(|options| &mut options.lock_idle),
157 },
158 Row {
159 label: "Amount of time:",
160 keywords: "password protect protected lock idle minutes hours timeout",
161 control: Control::Field(lock_after),
162 },
163 Row {
164 label: "Lock password protected sections as soon as I navigate away \
165 from them",
166 keywords: "password protect protected lock leave switch security",
167 control: Control::Check(|options| &mut options.lock_on_leave),
168 },
167 Row::new(
168 "Lock when idle for:",
169 "password protect protected lock unlock idle minutes hours timeout \
170 security",
171 Control::Field(lock_after),
172 )
173 .hint("Password-protected sections you haven't worked in"),
174 Row::new(
175 "Lock when I leave a section",
176 "password protect protected lock navigate away switch security",
177 Control::Check(|options| &mut options.lock_on_leave),
178 ),
169179 ],
170180 },
171181 ],
......@@ -175,55 +185,61 @@ const SECTIONS: &[Section] = &[
175185 groups: &[Group {
176186 heading: "",
177187 rows: &[
178 Row {
179 label: "Page tabs appear on the left",
180 keywords: "pages list side layout",
181 control: Control::Check(|options| &mut options.page_tabs_left),
182 },
183 Row {
184 label: "Navigation bar appears on the left",
185 keywords: "notebooks sidebar side right layout",
186 control: Control::Check(|options| &mut options.navigation_bar_left),
187 },
188 Row {
189 label: "Renderer:",
190 keywords: "graphics gpu backend drawing metal opengl vulkan direct3d \
191 webgpu webgl canvas",
192 control: Control::Field(renderer),
193 },
188 Row::new(
189 "Page tabs:",
190 "pages list side left right layout",
191 Control::Field(|state, options, name| {
192 side(state, name, &mut options.page_tabs_left)
193 }),
194 ),
195 Row::new(
196 "Notebooks:",
197 "navigation bar sidebar side left right layout",
198 Control::Field(|state, options, name| {
199 side(state, name, &mut options.navigation_bar_left)
200 }),
201 ),
202 Row::new(
203 "Renderer:",
204 "graphics gpu backend drawing metal opengl vulkan direct3d webgpu webgl \
205 canvas",
206 Control::Field(renderer),
207 ),
194208 ],
195209 }],
196210 },
197211 Section {
198212 name: "Sync & Storage",
199213 groups: &[
214 #[cfg(not(target_arch = "wasm32"))]
200215 Group {
201 heading: "Cache file location",
202 rows: &[Row {
203 label: "Path:",
204 keywords: "folder replica data disk",
205 control: Control::Field(cache),
206 }],
216 heading: "Cache",
217 rows: &[Row::new(
218 "Location:",
219 "cache file folder path replica data disk",
220 Control::Field(cache),
221 )
222 .hint("Where Snowbound keeps a copy of each section it syncs")],
207223 },
208224 #[cfg(feature = "live")]
209225 Group {
210226 heading: "Live Share",
211227 rows: &[
212 Row {
213 label: "Show others where I am in shared notebooks",
214 keywords: "presence privacy cursor caret avatar collaborate people",
215 control: Control::Check(|options| &mut options.presence),
216 },
217 Row {
218 label: "Show my account picture with my name",
219 keywords: "presence privacy photo avatar",
220 control: Control::Check(|options| &mut options.picture),
221 },
222 Row {
223 label: "Relay:",
224 keywords: "live share server internet network presence",
225 control: Control::Field(relay),
226 },
228 Row::new(
229 "Show others where I am in shared notebooks",
230 "presence privacy cursor caret avatar collaborate people",
231 Control::Check(|options| &mut options.presence),
232 ),
233 Row::new(
234 "Show my account picture with my name",
235 "presence privacy photo avatar",
236 Control::Check(|options| &mut options.picture),
237 ),
238 Row::new(
239 "Relay:",
240 "live share server internet network presence",
241 Control::Field(relay),
242 ),
227243 ],
228244 },
229245 ],
......@@ -233,22 +249,19 @@ const SECTIONS: &[Section] = &[
233249 groups: &[Group {
234250 heading: "",
235251 rows: &[
236 Row {
237 label: "Version:",
238 keywords: "about build release",
239 control: Control::Field(version),
240 },
241 Row {
242 label: "Check for updates automatically",
243 keywords: "update download",
244 control: Control::Check(|options| &mut options.automatic_updates),
245 },
252 Row::new("Version:", "about build release", Control::Field(version)),
253 #[cfg(not(target_arch = "wasm32"))]
254 Row::new(
255 "Check for updates automatically",
256 "update download",
257 Control::Check(|options| &mut options.automatic_updates),
258 ),
246259 #[cfg(target_os = "linux")]
247 Row {
248 label: "Installed:",
249 keywords: "install uninstall app menu remove",
250 control: Control::Field(installed),
251 },
260 Row::new(
261 "Installed:",
262 "install uninstall app menu remove",
263 Control::Field(installed),
264 ),
252265 ],
253266 }],
254267 },
......@@ -256,14 +269,14 @@ const SECTIONS: &[Section] = &[
256269 name: "Keyboard",
257270 groups: &[Group {
258271 heading: "",
259 rows: &[Row {
260 label: "Shortcuts",
261 keywords: "keys chords hotkeys bindings commands menus",
262 control: Control::Block {
272 rows: &[Row::new(
273 "Shortcuts",
274 "keys chords hotkeys bindings commands menus",
275 Control::Block {
263276 build: |state, options, query| options.keyboard.build(&mut state.ui, query),
264277 finds: crate::keys::searched,
265278 },
266 }],
279 )],
267280 }],
268281 },
269282];
......@@ -283,9 +296,8 @@ pub struct Options {
283296 default_font: DefaultFont,
284297 page_tabs_left: bool,
285298 navigation_bar_left: bool,
286 lock_idle: bool,
287 /// Minutes, as `protection::AFTER` lists them.
288 lock_minutes: u32,
299 /// Minutes, as `protection::AFTER` lists them; none never locks.
300 lock_after: Option<u32>,
289301 lock_on_leave: bool,
290302 presence: bool,
291303 picture: bool,
......@@ -312,34 +324,14 @@ fn user_name() -> Id {
312324 id().child("user-name")
313325}
314326
315fn schemes() -> Id {
316 id().child("color-schemes")
317}
318
319327fn fonts() -> Id {
320328 id().child("fonts")
321329}
322330
323fn sizes() -> Id {
324 id().child("sizes")
325}
326
327fn lock_times() -> Id {
328 id().child("lock-times")
329}
330
331331fn font_colors() -> Id {
332332 id().child("font-colors")
333333}
334334
335fn notebook_themes() -> Id {
336 id().child("notebook-themes")
337}
338
339fn renderers() -> Id {
340 id().child("renderers")
341}
342
343335/// Whether `word`, lowercase, starts a word of `text`: "pen" finds "Pen" and "pen-like",
344336/// not "Open".
345337fn starts_word(text: &str, word: &str) -> bool {
......@@ -394,8 +386,7 @@ impl State {
394386 default_font: self.view.editor.default_font.clone(),
395387 page_tabs_left: self.page_tabs_left,
396388 navigation_bar_left: !self.navigation_bar_right,
397 lock_idle: self.passwords.lock_after.is_some(),
398 lock_minutes: self.passwords.lock_after.unwrap_or(10),
389 lock_after: self.passwords.lock_after,
399390 lock_on_leave: self.passwords.lock_on_leave,
400391 presence: self.live_options.presence,
401392 picture: self.live_options.picture,
......@@ -409,7 +400,8 @@ impl State {
409400 }
410401
411402 /// Builds the Options dialog while it is open. OK, or Enter in the user name, keeps its
412 /// choices with a non-empty user name; Cancel, Escape or a press outside leave them.
403 /// choices, and a user name that isn't blank; Cancel, Escape or a press outside leave
404 /// them.
413405 pub(crate) fn options_dialog(&mut self) {
414406 let Some(mut options) = self.options.take() else {
415407 return;
......@@ -420,6 +412,7 @@ impl State {
420412 let theme = self.ui.theme.clone();
421413 let row = row_height(&theme);
422414 let ui = &mut self.ui;
415 let column = labels_column(ui);
423416 let entered = ui::popup::navigation(ui, &[user_name()], &[NamedKey::Enter])
424417 .contains(&NamedKey::Enter);
425418 // As tall as its contents up to the window, which the list then scrolls within.
......@@ -636,28 +629,11 @@ impl State {
636629 ..Spec::default()
637630 },
638631 );
639 match row.control {
640 Control::Check(choice) => {
641 let checked = choice(&mut options);
642 if ui::check_box(&mut self.ui, "check", row.label, *checked).clicked {
643 *checked = !*checked;
644 }
645 }
646 Control::Field(build) => {
647 self.ui.leaf(
648 "label",
649 Spec {
650 size: [px(LABEL), px(row_height(&theme))],
651 text: Some(row.label),
652 ..Spec::default()
653 },
654 );
655 build(self, &mut options);
656 }
657 Control::Block { build, .. } => {
658 let titled = titled(&query, section, group, row);
659 build(self, &mut options, if titled { &[] } else { &query });
660 }
632 if let Control::Block { build, .. } = row.control {
633 let titled = titled(&query, section, group, row);
634 build(self, &mut options, if titled { &[] } else { &query });
635 } else {
636 self.row(&mut options, row, column);
661637 }
662638 self.ui.close();
663639 }
......@@ -694,9 +670,11 @@ impl State {
694670 let ok = ok || entered;
695671 ui.close();
696672 ui.close();
697 let name = options.user_name.trim();
698 if ok && !name.is_empty() {
699 self.author = name.to_owned();
673 if ok {
674 let name = options.user_name.trim();
675 if !name.is_empty() {
676 self.author = name.to_owned();
677 }
700678 self.color_scheme = options.color_scheme;
701679 self.light_pages = !options.pages_match;
702680 self.hide_spelling = options.hide_spelling;
......@@ -706,7 +684,7 @@ impl State {
706684 self.page_tabs_left = options.page_tabs_left;
707685 self.navigation_bar_right = !options.navigation_bar_left;
708686 self.passwords = crate::settings::Passwords {
709 lock_after: options.lock_idle.then_some(options.lock_minutes),
687 lock_after: options.lock_after,
710688 lock_on_leave: options.lock_on_leave,
711689 };
712690 self.notebook_theme = options.notebook_theme;
......@@ -734,10 +712,79 @@ impl State {
734712 }
735713 self.ui.close_popup(id());
736714 }
715
716 /// A row's label in the labels' column `column` wide, then its control over its hint.
717 fn row(&mut self, options: &mut Options, row: &Row, column: f32) {
718 let theme = self.ui.theme.clone();
719 let height = row_height(&theme);
720 let field = matches!(row.control, Control::Field(_));
721 self.ui.leaf(
722 "label",
723 Spec {
724 size: [px(column), px(height)],
725 text: field.then_some(row.label),
726 ..Spec::default()
727 },
728 );
729 self.ui.open(
730 "control",
731 Spec {
732 axis: Axis::Y,
733 size: [fill(), ui::children()],
734 ..Spec::default()
735 },
736 );
737 match row.control {
738 Control::Check(choice) => {
739 let checked = choice(options);
740 if ui::check_box(&mut self.ui, "check", row.label, *checked).clicked {
741 *checked = !*checked;
742 }
743 }
744 Control::Field(build) => {
745 self.ui.open(
746 "field",
747 Spec {
748 size: [fill(), ui::children()],
749 gap: 8.0,
750 ..Spec::default()
751 },
752 );
753 build(self, options, row.label.trim_end_matches(':'));
754 self.ui.close();
755 }
756 Control::Block { .. } => {}
757 }
758 if !row.hint.is_empty() {
759 self.ui.leaf(
760 "hint",
761 Spec {
762 size: [fill(), ui::fit()],
763 text: Some(row.hint),
764 font_size: Some((theme.font_size * 0.92).round()),
765 color: Some(theme.text_dim),
766 overflow: ui::Overflow::Wrap,
767 pad: [0.0, 3.0],
768 ..Spec::default()
769 },
770 );
771 }
772 self.ui.close();
773 }
774}
775
776/// How wide the labels' column stands: as the widest field label, so every control lines up.
777fn labels_column(ui: &mut Ui) -> f32 {
778 let labels = (SECTIONS.iter())
779 .flat_map(|section| section.groups)
780 .flat_map(|group| group.rows)
781 .filter(|row| matches!(row.control, Control::Field(_)));
782 let widest = labels.fold(0.0_f32, |widest, row| widest.max(ui.measure(row.label)[0]));
783 (widest + 12.0).ceil()
737784}
738785
739786#[cfg(feature = "live")]
740fn relay(state: &mut State, options: &mut Options) {
787fn relay(state: &mut State, options: &mut Options, name: &str) {
741788 let ui = &mut state.ui;
742789 let theme = ui.theme.clone();
743790 let field = id().child("relay");
......@@ -756,7 +803,7 @@ fn relay(state: &mut State, options: &mut Options) {
756803 },
757804 );
758805 if let Some(node) = ui.access(field) {
759 node.set_label("Live Share relay");
806 node.set_label(name);
760807 }
761808}
762809
......@@ -806,62 +853,60 @@ fn heading(ui: &mut Ui, theme: &Theme, text: &str, lit: bool) {
806853 );
807854}
808855
809fn appearance(state: &mut State, options: &mut Options) {
810 let ui = &mut state.ui;
856/// A combo named `name` showing `choices[chosen]`, `width` wide, whose menu picks another.
857fn dropdown(ui: &mut Ui, name: &str, choices: &[&str], chosen: usize, width: f32) -> Option<usize> {
858 let menu = id().child(name);
811859 let combo = ui.id("combo");
812 let current = SCHEMES
813 .iter()
814 .find(|(scheme, _)| *scheme == options.color_scheme)
815 .map_or("", |(_, name)| name);
816 ui::shell::combo(ui, "combo", "Appearance", current, 140.0, schemes(), true);
817 let items = SCHEMES.map(|(scheme, name)| Item {
818 text: name,
819 checked: Some(scheme == options.color_scheme),
820 current: scheme == options.color_scheme,
821 ..Item::default()
822 });
860 let current = choices.get(chosen).copied().unwrap_or_default();
861 ui::shell::combo(ui, "combo", name, current, width, menu, true);
862 let items: Vec<Item> = (choices.iter().enumerate())
863 .map(|(index, text)| Item {
864 text,
865 checked: Some(index == chosen),
866 current: index == chosen,
867 ..Item::default()
868 })
869 .collect();
823870 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
824 if let Some(index) = ui::popup::menu(ui, schemes(), anchor, &items, None) {
871 ui::popup::menu(ui, menu, anchor, &items, None)
872}
873
874fn appearance(state: &mut State, options: &mut Options, name: &str) {
875 let chosen = (SCHEMES.iter())
876 .position(|(scheme, _)| *scheme == options.color_scheme)
877 .unwrap_or_default();
878 let choices = SCHEMES.map(|(_, label)| label);
879 if let Some(index) = ui::segmented(&mut state.ui, "schemes", name, &choices, chosen) {
825880 options.color_scheme = SCHEMES[index].0;
826881 }
827882}
828883
829fn notebook_theme(state: &mut State, options: &mut Options) {
830 let ui = &mut state.ui;
831 let combo = ui.id("combo");
884/// Left or Right, for a pane on the left where `left`.
885fn side(state: &mut State, name: &str, left: &mut bool) {
886 if let Some(index) = ui::segmented(&mut state.ui, "side", name, &SIDES, usize::from(!*left)) {
887 *left = index == 0;
888 }
889}
890
891fn notebook_theme(state: &mut State, options: &mut Options, name: &str) {
832892 let themes = notebook::sidecar::themes::built_in();
833 let choices: Vec<(Option<&str>, &str)> = std::iter::once((None, "No Theme"))
834 .chain(
835 themes
836 .iter()
837 .map(|theme| (Some(theme.id.as_str()), theme.name.as_str())),
838 )
893 let ids: Vec<Option<&str>> = std::iter::once(None)
894 .chain(themes.iter().map(|theme| Some(theme.id.as_str())))
839895 .collect();
840 let chosen = options.notebook_theme.as_deref();
841 let current = choices
842 .iter()
843 .find(|(id, _)| *id == chosen)
844 .map_or("", |(_, name)| name);
845 let title = "Default for new notebooks";
846 ui::shell::combo(ui, "combo", title, current, 140.0, notebook_themes(), true);
847 let items: Vec<Item> = choices
848 .iter()
849 .map(|(id, name)| Item {
850 text: name,
851 checked: Some(*id == chosen),
852 current: *id == chosen,
853 ..Item::default()
854 })
896 let labels: Vec<&str> = std::iter::once("No Theme")
897 .chain(themes.iter().map(|theme| theme.name.as_str()))
855898 .collect();
856 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
857 if let Some(index) = ui::popup::menu(ui, notebook_themes(), anchor, &items, None) {
858 options.notebook_theme = choices[index].0.map(Into::into);
899 let chosen = (ids.iter())
900 .position(|id| *id == options.notebook_theme.as_deref())
901 .unwrap_or_default();
902 if let Some(index) = dropdown(&mut state.ui, name, &labels, chosen, 160.0) {
903 options.notebook_theme = ids[index].map(Into::into);
859904 }
860905}
861906
862907/// The platform's backends, the one the default starts with marked, those that didn't start
863908/// this run disabled; then the backend and adapter drawing now.
864fn renderer(state: &mut State, options: &mut Options) {
909fn renderer(state: &mut State, options: &mut Options, name: &str) {
865910 let State {
866911 ui,
867912 unavailable,
......@@ -892,7 +937,8 @@ fn renderer(state: &mut State, options: &mut Options) {
892937 let labels: Vec<String> = Backend::PLATFORM.iter().copied().map(label).collect();
893938 let current = shown.map(label).unwrap_or_default();
894939 let combo = ui.id("combo");
895 ui::shell::combo(ui, "combo", "Renderer", &current, 160.0, renderers(), true);
940 let menu = id().child(name);
941 ui::shell::combo(ui, "combo", name, &current, 160.0, menu, true);
896942 let items: Vec<Item> = (Backend::PLATFORM.iter().zip(&labels))
897943 .map(|(backend, text)| Item {
898944 text,
......@@ -904,7 +950,7 @@ fn renderer(state: &mut State, options: &mut Options) {
904950 })
905951 .collect();
906952 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
907 if let Some(index) = ui::popup::menu(ui, renderers(), anchor, &items, None) {
953 if let Some(index) = ui::popup::menu(ui, menu, anchor, &items, None) {
908954 let picked = Backend::PLATFORM[index];
909955 options.renderer = if Some(picked) == automatic {
910956 Backend::Default
......@@ -928,36 +974,22 @@ fn renderer(state: &mut State, options: &mut Options) {
928974 ui.leaf("adapter", spec);
929975}
930976
931fn lock_after(state: &mut State, options: &mut Options) {
932 let ui = &mut state.ui;
933 let combo = ui.id("combo");
934 let current = crate::protection::AFTER
935 .iter()
936 .find(|(minutes, _)| *minutes == options.lock_minutes)
937 .map_or("", |(_, name)| name);
938 ui::shell::combo(
939 ui,
940 "combo",
941 "Amount of time",
942 current,
943 140.0,
944 lock_times(),
945 true,
946 );
947 let items = crate::protection::AFTER.map(|(minutes, name)| Item {
948 text: name,
949 checked: Some(minutes == options.lock_minutes),
950 current: minutes == options.lock_minutes,
951 ..Item::default()
952 });
953 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
954 if let Some(index) = ui::popup::menu(ui, lock_times(), anchor, &items, None) {
955 options.lock_minutes = crate::protection::AFTER[index].0;
956 options.lock_idle = true;
977/// Never, or one of OneNote's times, `protection::AFTER`.
978fn lock_after(state: &mut State, options: &mut Options, name: &str) {
979 let after = crate::protection::AFTER;
980 let labels: Vec<&str> = std::iter::once("Never")
981 .chain(after.iter().map(|(_, label)| *label))
982 .collect();
983 let chosen = options
984 .lock_after
985 .and_then(|minutes| after.iter().position(|(each, _)| *each == minutes))
986 .map_or(0, |index| index + 1);
987 if let Some(index) = dropdown(&mut state.ui, name, &labels, chosen, 160.0) {
988 options.lock_after = index.checked_sub(1).map(|index| after[index].0);
957989 }
958990}
959991
960fn user_name_field(state: &mut State, options: &mut Options) {
992fn user_name_field(state: &mut State, options: &mut Options, name: &str) {
961993 let ui = &mut state.ui;
962994 let theme = &ui.theme;
963995 let width = Extent {
......@@ -974,53 +1006,52 @@ fn user_name_field(state: &mut State, options: &mut Options) {
9741006 };
9751007 ui::text_field(ui, user_name(), &mut options.user_name, "", spec);
9761008 if let Some(node) = ui.access(user_name()) {
977 node.set_label("User name");
1009 node.set_label(name);
9781010 }
9791011}
9801012
981fn font_face(state: &mut State, options: &mut Options) {
1013/// The face, from the font menu, and its size.
1014fn font_face(state: &mut State, options: &mut Options, name: &str) {
9821015 let State { ui, fonts, .. } = state;
9831016 let font = &mut options.default_font;
9841017 let combo = ui.id("combo");
985 ui::shell::combo(ui, "combo", "Font", &font.face, 200.0, self::fonts(), true);
1018 ui::shell::combo(ui, "combo", name, &font.face, 200.0, self::fonts(), true);
9861019 let items: Vec<_> = fonts
9871020 .iter()
988 .map(|name| Item {
989 text: name,
990 font: Some(name),
991 current: *name == font.face,
1021 .map(|face| Item {
1022 text: face,
1023 font: Some(face),
1024 current: *face == font.face,
9921025 ..Item::default()
9931026 })
9941027 .collect();
9951028 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
996 if let Some(index) = ui::popup::menu(ui, self::fonts(), anchor, &items, Some("Font")) {
1029 if let Some(index) = ui::popup::menu(ui, self::fonts(), anchor, &items, Some(name)) {
9971030 font.face = fonts[index].clone();
9981031 }
999}
1000
1001fn font_size(state: &mut State, options: &mut Options) {
1002 let ui = &mut state.ui;
1003 let font = &mut options.default_font;
1004 let combo = ui.id("combo");
1005 let size = format!("{}", font.size);
1006 ui::shell::combo(ui, "combo", "Size", &size, 60.0, sizes(), true);
10071032 let labels = SIZES.map(|size| format!("{size}"));
1008 let items: Vec<_> = labels
1009 .iter()
1010 .map(|label| Item {
1011 text: label,
1012 checked: Some(*label == size),
1013 current: *label == size,
1014 ..Item::default()
1015 })
1016 .collect();
1017 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
1018 if let Some(index) = ui::popup::menu(ui, sizes(), anchor, &items, None) {
1019 font.size = SIZES[index];
1033 let shown = format!("{}", font.size);
1034 let mut choices: Vec<&str> = labels.iter().map(String::as_str).collect();
1035 // A size typed in the toolbar's box may be none of OneNote's.
1036 let chosen = SIZES.iter().position(|size| *size == font.size);
1037 let chosen = chosen.unwrap_or_else(|| {
1038 choices.push(&shown);
1039 choices.len() - 1
1040 });
1041 ui.open(
1042 "size",
1043 Spec {
1044 size: [ui::children(), ui::children()],
1045 ..Spec::default()
1046 },
1047 );
1048 if let Some(index) = dropdown(ui, "Size", &choices, chosen, 64.0) {
1049 font.size = SIZES.get(index).copied().unwrap_or(font.size);
10201050 }
1051 ui.close();
10211052}
10221053
1023fn font_color(state: &mut State, options: &mut Options) {
1054fn font_color(state: &mut State, options: &mut Options, name: &str) {
10241055 let ui = &mut state.ui;
10251056 let font = &mut options.default_font;
10261057 let combo = ui.id("combo");
......@@ -1028,7 +1059,7 @@ fn font_color(state: &mut State, options: &mut Options) {
10281059 .iter()
10291060 .find(|(color, _)| Some(*color) == font.color)
10301061 .map_or("Automatic", |(_, name)| name);
1031 ui::shell::combo(ui, "combo", "Font color", shown, 200.0, font_colors(), true);
1062 ui::shell::combo(ui, "combo", name, shown, 200.0, font_colors(), true);
10321063 let swatches: Vec<_> = crate::FONT_COLORS
10331064 .iter()
10341065 .map(|&(color, name)| (canvas::gpu::colorref(color), name))
......@@ -1042,31 +1073,33 @@ fn font_color(state: &mut State, options: &mut Options) {
10421073 }
10431074}
10441075
1045/// A path shown dimmed and cut to the row.
1046fn path(ui: &mut Ui, path: &std::path::Path) {
1076/// Text shown dimmed and cut to the row, as a path.
1077#[cfg(not(target_arch = "wasm32"))]
1078fn dim(ui: &mut Ui, text: &str) {
10471079 let spec = Spec {
10481080 flags: Flags::CLIP,
10491081 size: [fill(), px(row_height(&ui.theme))],
1050 text: Some(&path.to_string_lossy()),
1082 text: Some(text),
10511083 color: Some(ui.theme.text_dim),
10521084 ..Spec::default()
10531085 };
1054 ui.leaf("path", spec);
1086 ui.leaf("dim", spec);
10551087}
10561088
1057fn cache(state: &mut State, _: &mut Options) {
1058 path(&mut state.ui, &state.cache);
1089#[cfg(not(target_arch = "wasm32"))]
1090fn cache(state: &mut State, _: &mut Options, _: &str) {
1091 dim(&mut state.ui, &state.cache.to_string_lossy());
10591092 let label = if cfg!(target_os = "macos") {
10601093 "Show in Finder"
10611094 } else {
10621095 "Open Folder"
10631096 };
10641097 if ui::button(&mut state.ui, "reveal", label).clicked {
1065 platform::reveal(&state.cache);
1098 crate::platform::reveal(&state.cache);
10661099 }
10671100}
10681101
1069fn version(state: &mut State, _: &mut Options) {
1102fn version(state: &mut State, _: &mut Options, _: &str) {
10701103 let ui = &mut state.ui;
10711104 let spec = Spec {
10721105 size: [fill(), px(row_height(&ui.theme))],
......@@ -1078,20 +1111,25 @@ fn version(state: &mut State, _: &mut Options) {
10781111
10791112/// Where Install put Snowbound and Uninstall, or Install where it isn't in the app menu.
10801113#[cfg(target_os = "linux")]
1081fn installed(state: &mut State, _: &mut Options) {
1114fn installed(state: &mut State, _: &mut Options, _: &str) {
10821115 let ui = &mut state.ui;
10831116 if crate::desktop::uninstallable() {
1084 path(ui, &crate::desktop::binary().unwrap_or_default());
1117 dim(
1118 ui,
1119 &crate::desktop::binary()
1120 .unwrap_or_default()
1121 .to_string_lossy(),
1122 );
10851123 if ui::button(ui, "uninstall", "Uninstall").clicked {
10861124 crate::desktop::uninstall(&state.proxy);
10871125 }
10881126 } else if crate::desktop::installable() {
1089 path(ui, std::path::Path::new("Not in the app menu"));
1127 dim(ui, "Not in the app menu");
10901128 if ui::button(ui, "install", "Install").clicked {
10911129 crate::desktop::install();
10921130 }
10931131 } else {
1094 path(ui, std::path::Path::new("By your system's package manager"));
1132 dim(ui, "By your system's package manager");
10951133 }
10961134}
10971135