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 @@...@@ -3,7 +3,7 @@
3//! chosen.3//! chosen.
44
5use crate::{5use crate::{
6 State, UserEvent, platform,6 State, UserEvent,
7 settings::{Backend, ColorScheme},7 settings::{Backend, ColorScheme},
8 update,8 update,
9};9};
...@@ -12,15 +12,14 @@ use canvas::editor::DefaultFont;...@@ -12,15 +12,14 @@ use canvas::editor::DefaultFont;
12use ui::{Anchor, Axis, Extent, Flags, Id, Size, Spec, Theme, Ui, fill, popup::Item, px};12use ui::{Anchor, Axis, Extent, Flags, Id, Size, Spec, Theme, Ui, fill, popup::Item, px};
13use winit::keyboard::NamedKey;13use winit::keyboard::NamedKey;
1414
15const WIDTH: f32 = 680.0;15const WIDTH: f32 = 700.0;
16const NAV: f32 = 150.0;16const NAV: f32 = 150.0;
17/// The column the fields' labels share.
18const LABEL: f32 = 104.0;
19const SCHEMES: [(ColorScheme, &str); 3] = [17const SCHEMES: [(ColorScheme, &str); 3] = [
20 (ColorScheme::System, "System"),18 (ColorScheme::System, "System"),
21 (ColorScheme::Light, "Light"),19 (ColorScheme::Light, "Light"),
22 (ColorScheme::Dark, "Dark"),20 (ColorScheme::Dark, "Dark"),
23];21];
22const SIDES: [&str; 2] = ["Left", "Right"];
24/// The sizes Default font offers, OneNote 2010's list in points.23/// The sizes Default font offers, OneNote 2010's list in points.
25const SIZES: [f32; 19] = [24const SIZES: [f32; 19] = [
26 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,25 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 {...@@ -42,16 +41,35 @@ struct Group {
4241
43struct Row {42struct Row {
44 label: &'static str,43 label: &'static str,
44 /// A line under the control for what the label can't say; empty for none.
45 hint: &'static str,
45 /// Other words search finds the row by.46 /// Other words search finds the row by.
46 keywords: &'static str,47 keywords: &'static str,
47 control: Control,48 control: Control,
48}49}
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.
50enum Control {67enum Control {
51 /// A check box before the label, bound to a choice.68 /// A check box labelled by the row, bound to a choice.
52 Check(fn(&mut Options) -> &mut bool),69 Check(fn(&mut Options) -> &mut bool),
53 /// The label in the labels' column, then what the function builds.70 /// The row's label in the labels' column, then what the function builds, given the
54 Field(fn(&mut State, &mut Options)),71 /// label to name its control by.
72 Field(fn(&mut State, &mut Options, &str)),
55 /// What `build` makes across the row, unlabelled, from the search's words; `finds` tells73 /// What `build` makes across the row, unlabelled, from the search's words; `finds` tells
56 /// whether they find anything in it. Words found in the row's own texts give it none.74 /// whether they find anything in it. Words found in the row's own texts give it none.
57 Block {75 Block {
...@@ -60,33 +78,33 @@ enum Control {...@@ -60,33 +78,33 @@ enum Control {
60 },78 },
61}79}
6280
63/// Every section, in the list's order.81/// Every section, in the list's order, with only the rows that apply here.
64const SECTIONS: &[Section] = &[82const SECTIONS: &[Section] = &[
65 Section {83 Section {
66 name: "General",84 name: "General",
67 groups: &[85 groups: &[
68 Group {86 Group {
69 heading: "User Interface Options",87 heading: "Interface",
70 rows: &[88 rows: &[
71 Row {89 Row::new(
72 label: "Appearance:",90 "Appearance:",
73 keywords: "color colour scheme dark light mode system theme",91 "color colour scheme dark light mode system theme",
74 control: Control::Field(appearance),92 Control::Field(appearance),
75 },93 ),
76 Row {94 Row::new(
77 label: "Pages match UI theme",95 "Pages match the appearance",
78 keywords: "dark white paper background",96 "dark white paper background ui theme",
79 control: Control::Check(|options| &mut options.pages_match),97 Control::Check(|options| &mut options.pages_match),
80 },98 )
99 .hint("Turned off, pages stay white"),
81 ],100 ],
82 },101 },
83 Group {102 Group {
84 heading: "Personalize",103 heading: "Personalize",
85 rows: &[Row {104 rows: &[
86 label: "User name:",105 Row::new("User name:", "author", Control::Field(user_name_field))
87 keywords: "author name",106 .hint("Shown with your edits in shared notebooks"),
88 control: Control::Field(user_name_field),107 ],
89 }],
90 },108 },
91 ],109 ],
92 },110 },
...@@ -96,76 +114,68 @@ const SECTIONS: &[Section] = &[...@@ -96,76 +114,68 @@ const SECTIONS: &[Section] = &[
96 Group {114 Group {
97 heading: "Default font",115 heading: "Default font",
98 rows: &[116 rows: &[
99 Row {117 Row::new(
100 label: "Font:",118 "Font:",
101 keywords: "typeface text new",119 "typeface size points text new",
102 control: Control::Field(font_face),120 Control::Field(font_face),
103 },121 ),
104 Row {122 Row::new(
105 label: "Size:",123 "Color:",
106 keywords: "font points text new",124 "font colour text new automatic",
107 control: Control::Field(font_size),125 Control::Field(font_color),
108 },126 ),
109 Row {
110 label: "Font color:",
111 keywords: "colour text new automatic",
112 control: Control::Field(font_color),
113 },
114 ],127 ],
115 },128 },
116 Group {129 Group {
117 heading: "Default style theme",130 heading: "Font styles",
118 rows: &[Row {131 rows: &[Row::new(
119 label: "New notebooks:",132 "New notebooks:",
120 keywords: "default for new notebooks styles headings modern",133 "default style theme headings modern",
121 control: Control::Field(notebook_theme),134 Control::Field(notebook_theme),
122 }],135 )
136 .hint("The styles a new notebook starts with")],
123 },137 },
124 Group {138 Group {
125 heading: "Proofing",139 heading: "Proofing",
126 rows: &[Row {140 rows: &[Row::new(
127 label: "Hide spelling errors",141 "Hide spelling errors",
128 keywords: "spell check misspelled words",142 "spell check misspelled words",
129 control: Control::Check(|options| &mut options.hide_spelling),143 Control::Check(|options| &mut options.hide_spelling),
130 }],144 )],
131 },145 },
132 Group {146 Group {
133 heading: "AutoFormat",147 heading: "AutoFormat",
134 rows: &[Row {148 rows: &[Row::new(
135 label: "Markdown shortcuts",149 "Markdown shortcuts",
136 keywords: "markdown autoformat heading bullet numbering list to do quote \150 "markdown autoformat heading bullet numbering list to do quote bold \
137 bold italic strikethrough code typing",151 italic strikethrough code typing",
138 control: Control::Check(|options| &mut options.markdown),152 Control::Check(|options| &mut options.markdown),
139 }],153 )
154 .hint("Type # for a heading, - for a bullet, **bold** and more")],
140 },155 },
141 Group {156 Group {
142 heading: "Pen",157 heading: "Pen",
143 rows: &[Row {158 rows: &[Row::new(
144 label: "Use pen pressure sensitivity",159 "Use pen pressure sensitivity",
145 keywords: "tablet stylus ink stroke width drawing",160 "tablet stylus ink stroke width drawing",
146 control: Control::Check(|options| &mut options.pen_pressure),161 Control::Check(|options| &mut options.pen_pressure),
147 }],162 )],
148 },163 },
149 Group {164 Group {
150 heading: "Passwords",165 heading: "Passwords",
151 rows: &[166 rows: &[
152 Row {167 Row::new(
153 label: "Lock password protected sections after I have not worked in \168 "Lock when idle for:",
154 them for:",169 "password protect protected lock unlock idle minutes hours timeout \
155 keywords: "protect protected lock unlock idle minutes timeout security",170 security",
156 control: Control::Check(|options| &mut options.lock_idle),171 Control::Field(lock_after),
157 },172 )
158 Row {173 .hint("Password-protected sections you haven't worked in"),
159 label: "Amount of time:",174 Row::new(
160 keywords: "password protect protected lock idle minutes hours timeout",175 "Lock when I leave a section",
161 control: Control::Field(lock_after),176 "password protect protected lock navigate away switch security",
162 },177 Control::Check(|options| &mut options.lock_on_leave),
163 Row {178 ),
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 },
169 ],179 ],
170 },180 },
171 ],181 ],
...@@ -175,55 +185,61 @@ const SECTIONS: &[Section] = &[...@@ -175,55 +185,61 @@ const SECTIONS: &[Section] = &[
175 groups: &[Group {185 groups: &[Group {
176 heading: "",186 heading: "",
177 rows: &[187 rows: &[
178 Row {188 Row::new(
179 label: "Page tabs appear on the left",189 "Page tabs:",
180 keywords: "pages list side layout",190 "pages list side left right layout",
181 control: Control::Check(|options| &mut options.page_tabs_left),191 Control::Field(|state, options, name| {
182 },192 side(state, name, &mut options.page_tabs_left)
183 Row {193 }),
184 label: "Navigation bar appears on the left",194 ),
185 keywords: "notebooks sidebar side right layout",195 Row::new(
186 control: Control::Check(|options| &mut options.navigation_bar_left),196 "Notebooks:",
187 },197 "navigation bar sidebar side left right layout",
188 Row {198 Control::Field(|state, options, name| {
189 label: "Renderer:",199 side(state, name, &mut options.navigation_bar_left)
190 keywords: "graphics gpu backend drawing metal opengl vulkan direct3d \200 }),
191 webgpu webgl canvas",201 ),
192 control: Control::Field(renderer),202 Row::new(
193 },203 "Renderer:",
204 "graphics gpu backend drawing metal opengl vulkan direct3d webgpu webgl \
205 canvas",
206 Control::Field(renderer),
207 ),
194 ],208 ],
195 }],209 }],
196 },210 },
197 Section {211 Section {
198 name: "Sync & Storage",212 name: "Sync & Storage",
199 groups: &[213 groups: &[
214 #[cfg(not(target_arch = "wasm32"))]
200 Group {215 Group {
201 heading: "Cache file location",216 heading: "Cache",
202 rows: &[Row {217 rows: &[Row::new(
203 label: "Path:",218 "Location:",
204 keywords: "folder replica data disk",219 "cache file folder path replica data disk",
205 control: Control::Field(cache),220 Control::Field(cache),
206 }],221 )
222 .hint("Where Snowbound keeps a copy of each section it syncs")],
207 },223 },
208 #[cfg(feature = "live")]224 #[cfg(feature = "live")]
209 Group {225 Group {
210 heading: "Live Share",226 heading: "Live Share",
211 rows: &[227 rows: &[
212 Row {228 Row::new(
213 label: "Show others where I am in shared notebooks",229 "Show others where I am in shared notebooks",
214 keywords: "presence privacy cursor caret avatar collaborate people",230 "presence privacy cursor caret avatar collaborate people",
215 control: Control::Check(|options| &mut options.presence),231 Control::Check(|options| &mut options.presence),
216 },232 ),
217 Row {233 Row::new(
218 label: "Show my account picture with my name",234 "Show my account picture with my name",
219 keywords: "presence privacy photo avatar",235 "presence privacy photo avatar",
220 control: Control::Check(|options| &mut options.picture),236 Control::Check(|options| &mut options.picture),
221 },237 ),
222 Row {238 Row::new(
223 label: "Relay:",239 "Relay:",
224 keywords: "live share server internet network presence",240 "live share server internet network presence",
225 control: Control::Field(relay),241 Control::Field(relay),
226 },242 ),
227 ],243 ],
228 },244 },
229 ],245 ],
...@@ -233,22 +249,19 @@ const SECTIONS: &[Section] = &[...@@ -233,22 +249,19 @@ const SECTIONS: &[Section] = &[
233 groups: &[Group {249 groups: &[Group {
234 heading: "",250 heading: "",
235 rows: &[251 rows: &[
236 Row {252 Row::new("Version:", "about build release", Control::Field(version)),
237 label: "Version:",253 #[cfg(not(target_arch = "wasm32"))]
238 keywords: "about build release",254 Row::new(
239 control: Control::Field(version),255 "Check for updates automatically",
240 },256 "update download",
241 Row {257 Control::Check(|options| &mut options.automatic_updates),
242 label: "Check for updates automatically",258 ),
243 keywords: "update download",
244 control: Control::Check(|options| &mut options.automatic_updates),
245 },
246 #[cfg(target_os = "linux")]259 #[cfg(target_os = "linux")]
247 Row {260 Row::new(
248 label: "Installed:",261 "Installed:",
249 keywords: "install uninstall app menu remove",262 "install uninstall app menu remove",
250 control: Control::Field(installed),263 Control::Field(installed),
251 },264 ),
252 ],265 ],
253 }],266 }],
254 },267 },
...@@ -256,14 +269,14 @@ const SECTIONS: &[Section] = &[...@@ -256,14 +269,14 @@ const SECTIONS: &[Section] = &[
256 name: "Keyboard",269 name: "Keyboard",
257 groups: &[Group {270 groups: &[Group {
258 heading: "",271 heading: "",
259 rows: &[Row {272 rows: &[Row::new(
260 label: "Shortcuts",273 "Shortcuts",
261 keywords: "keys chords hotkeys bindings commands menus",274 "keys chords hotkeys bindings commands menus",
262 control: Control::Block {275 Control::Block {
263 build: |state, options, query| options.keyboard.build(&mut state.ui, query),276 build: |state, options, query| options.keyboard.build(&mut state.ui, query),
264 finds: crate::keys::searched,277 finds: crate::keys::searched,
265 },278 },
266 }],279 )],
267 }],280 }],
268 },281 },
269];282];
...@@ -283,9 +296,8 @@ pub struct Options {...@@ -283,9 +296,8 @@ pub struct Options {
283 default_font: DefaultFont,296 default_font: DefaultFont,
284 page_tabs_left: bool,297 page_tabs_left: bool,
285 navigation_bar_left: bool,298 navigation_bar_left: bool,
286 lock_idle: bool,299 /// Minutes, as `protection::AFTER` lists them; none never locks.
287 /// Minutes, as `protection::AFTER` lists them.300 lock_after: Option<u32>,
288 lock_minutes: u32,
289 lock_on_leave: bool,301 lock_on_leave: bool,
290 presence: bool,302 presence: bool,
291 picture: bool,303 picture: bool,
...@@ -312,34 +324,14 @@ fn user_name() -> Id {...@@ -312,34 +324,14 @@ fn user_name() -> Id {
312 id().child("user-name")324 id().child("user-name")
313}325}
314326
315fn schemes() -> Id {
316 id().child("color-schemes")
317}
318
319fn fonts() -> Id {327fn fonts() -> Id {
320 id().child("fonts")328 id().child("fonts")
321}329}
322330
323fn sizes() -> Id {
324 id().child("sizes")
325}
326
327fn lock_times() -> Id {
328 id().child("lock-times")
329}
330
331fn font_colors() -> Id {331fn font_colors() -> Id {
332 id().child("font-colors")332 id().child("font-colors")
333}333}
334334
335fn notebook_themes() -> Id {
336 id().child("notebook-themes")
337}
338
339fn renderers() -> Id {
340 id().child("renderers")
341}
342
343/// Whether `word`, lowercase, starts a word of `text`: "pen" finds "Pen" and "pen-like",335/// Whether `word`, lowercase, starts a word of `text`: "pen" finds "Pen" and "pen-like",
344/// not "Open".336/// not "Open".
345fn starts_word(text: &str, word: &str) -> bool {337fn starts_word(text: &str, word: &str) -> bool {
...@@ -394,8 +386,7 @@ impl State {...@@ -394,8 +386,7 @@ impl State {
394 default_font: self.view.editor.default_font.clone(),386 default_font: self.view.editor.default_font.clone(),
395 page_tabs_left: self.page_tabs_left,387 page_tabs_left: self.page_tabs_left,
396 navigation_bar_left: !self.navigation_bar_right,388 navigation_bar_left: !self.navigation_bar_right,
397 lock_idle: self.passwords.lock_after.is_some(),389 lock_after: self.passwords.lock_after,
398 lock_minutes: self.passwords.lock_after.unwrap_or(10),
399 lock_on_leave: self.passwords.lock_on_leave,390 lock_on_leave: self.passwords.lock_on_leave,
400 presence: self.live_options.presence,391 presence: self.live_options.presence,
401 picture: self.live_options.picture,392 picture: self.live_options.picture,
...@@ -409,7 +400,8 @@ impl State {...@@ -409,7 +400,8 @@ impl State {
409 }400 }
410401
411 /// Builds the Options dialog while it is open. OK, or Enter in the user name, keeps its402 /// 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.
413 pub(crate) fn options_dialog(&mut self) {405 pub(crate) fn options_dialog(&mut self) {
414 let Some(mut options) = self.options.take() else {406 let Some(mut options) = self.options.take() else {
415 return;407 return;
...@@ -420,6 +412,7 @@ impl State {...@@ -420,6 +412,7 @@ impl State {
420 let theme = self.ui.theme.clone();412 let theme = self.ui.theme.clone();
421 let row = row_height(&theme);413 let row = row_height(&theme);
422 let ui = &mut self.ui;414 let ui = &mut self.ui;
415 let column = labels_column(ui);
423 let entered = ui::popup::navigation(ui, &[user_name()], &[NamedKey::Enter])416 let entered = ui::popup::navigation(ui, &[user_name()], &[NamedKey::Enter])
424 .contains(&NamedKey::Enter);417 .contains(&NamedKey::Enter);
425 // As tall as its contents up to the window, which the list then scrolls within.418 // As tall as its contents up to the window, which the list then scrolls within.
...@@ -636,28 +629,11 @@ impl State {...@@ -636,28 +629,11 @@ impl State {
636 ..Spec::default()629 ..Spec::default()
637 },630 },
638 );631 );
639 match row.control {632 if let Control::Block { build, .. } = row.control {
640 Control::Check(choice) => {633 let titled = titled(&query, section, group, row);
641 let checked = choice(&mut options);634 build(self, &mut options, if titled { &[] } else { &query });
642 if ui::check_box(&mut self.ui, "check", row.label, *checked).clicked {635 } else {
643 *checked = !*checked;636 self.row(&mut options, row, column);
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 }
661 }637 }
662 self.ui.close();638 self.ui.close();
663 }639 }
...@@ -694,9 +670,11 @@ impl State {...@@ -694,9 +670,11 @@ impl State {
694 let ok = ok || entered;670 let ok = ok || entered;
695 ui.close();671 ui.close();
696 ui.close();672 ui.close();
697 let name = options.user_name.trim();673 if ok {
698 if ok && !name.is_empty() {674 let name = options.user_name.trim();
699 self.author = name.to_owned();675 if !name.is_empty() {
676 self.author = name.to_owned();
677 }
700 self.color_scheme = options.color_scheme;678 self.color_scheme = options.color_scheme;
701 self.light_pages = !options.pages_match;679 self.light_pages = !options.pages_match;
702 self.hide_spelling = options.hide_spelling;680 self.hide_spelling = options.hide_spelling;
...@@ -706,7 +684,7 @@ impl State {...@@ -706,7 +684,7 @@ impl State {
706 self.page_tabs_left = options.page_tabs_left;684 self.page_tabs_left = options.page_tabs_left;
707 self.navigation_bar_right = !options.navigation_bar_left;685 self.navigation_bar_right = !options.navigation_bar_left;
708 self.passwords = crate::settings::Passwords {686 self.passwords = crate::settings::Passwords {
709 lock_after: options.lock_idle.then_some(options.lock_minutes),687 lock_after: options.lock_after,
710 lock_on_leave: options.lock_on_leave,688 lock_on_leave: options.lock_on_leave,
711 };689 };
712 self.notebook_theme = options.notebook_theme;690 self.notebook_theme = options.notebook_theme;
...@@ -734,10 +712,79 @@ impl State {...@@ -734,10 +712,79 @@ impl State {
734 }712 }
735 self.ui.close_popup(id());713 self.ui.close_popup(id());
736 }714 }
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()
737}784}
738785
739#[cfg(feature = "live")]786#[cfg(feature = "live")]
740fn relay(state: &mut State, options: &mut Options) {787fn relay(state: &mut State, options: &mut Options, name: &str) {
741 let ui = &mut state.ui;788 let ui = &mut state.ui;
742 let theme = ui.theme.clone();789 let theme = ui.theme.clone();
743 let field = id().child("relay");790 let field = id().child("relay");
...@@ -756,7 +803,7 @@ fn relay(state: &mut State, options: &mut Options) {...@@ -756,7 +803,7 @@ fn relay(state: &mut State, options: &mut Options) {
756 },803 },
757 );804 );
758 if let Some(node) = ui.access(field) {805 if let Some(node) = ui.access(field) {
759 node.set_label("Live Share relay");806 node.set_label(name);
760 }807 }
761}808}
762809
...@@ -806,62 +853,60 @@ fn heading(ui: &mut Ui, theme: &Theme, text: &str, lit: bool) {...@@ -806,62 +853,60 @@ fn heading(ui: &mut Ui, theme: &Theme, text: &str, lit: bool) {
806 );853 );
807}854}
808855
809fn appearance(state: &mut State, options: &mut Options) {856/// A combo named `name` showing `choices[chosen]`, `width` wide, whose menu picks another.
810 let ui = &mut state.ui;857fn dropdown(ui: &mut Ui, name: &str, choices: &[&str], chosen: usize, width: f32) -> Option<usize> {
858 let menu = id().child(name);
811 let combo = ui.id("combo");859 let combo = ui.id("combo");
812 let current = SCHEMES860 let current = choices.get(chosen).copied().unwrap_or_default();
813 .iter()861 ui::shell::combo(ui, "combo", name, current, width, menu, true);
814 .find(|(scheme, _)| *scheme == options.color_scheme)862 let items: Vec<Item> = (choices.iter().enumerate())
815 .map_or("", |(_, name)| name);863 .map(|(index, text)| Item {
816 ui::shell::combo(ui, "combo", "Appearance", current, 140.0, schemes(), true);864 text,
817 let items = SCHEMES.map(|(scheme, name)| Item {865 checked: Some(index == chosen),
818 text: name,866 current: index == chosen,
819 checked: Some(scheme == options.color_scheme),867 ..Item::default()
820 current: scheme == options.color_scheme,868 })
821 ..Item::default()869 .collect();
822 });
823 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());870 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) {
825 options.color_scheme = SCHEMES[index].0;880 options.color_scheme = SCHEMES[index].0;
826 }881 }
827}882}
828883
829fn notebook_theme(state: &mut State, options: &mut Options) {884/// Left or Right, for a pane on the left where `left`.
830 let ui = &mut state.ui;885fn side(state: &mut State, name: &str, left: &mut bool) {
831 let combo = ui.id("combo");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) {
832 let themes = notebook::sidecar::themes::built_in();892 let themes = notebook::sidecar::themes::built_in();
833 let choices: Vec<(Option<&str>, &str)> = std::iter::once((None, "No Theme"))893 let ids: Vec<Option<&str>> = std::iter::once(None)
834 .chain(894 .chain(themes.iter().map(|theme| Some(theme.id.as_str())))
835 themes
836 .iter()
837 .map(|theme| (Some(theme.id.as_str()), theme.name.as_str())),
838 )
839 .collect();895 .collect();
840 let chosen = options.notebook_theme.as_deref();896 let labels: Vec<&str> = std::iter::once("No Theme")
841 let current = choices897 .chain(themes.iter().map(|theme| theme.name.as_str()))
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 })
855 .collect();898 .collect();
856 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());899 let chosen = (ids.iter())
857 if let Some(index) = ui::popup::menu(ui, notebook_themes(), anchor, &items, None) {900 .position(|id| *id == options.notebook_theme.as_deref())
858 options.notebook_theme = choices[index].0.map(Into::into);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);
859 }904 }
860}905}
861906
862/// The platform's backends, the one the default starts with marked, those that didn't start907/// The platform's backends, the one the default starts with marked, those that didn't start
863/// this run disabled; then the backend and adapter drawing now.908/// 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) {
865 let State {910 let State {
866 ui,911 ui,
867 unavailable,912 unavailable,
...@@ -892,7 +937,8 @@ fn renderer(state: &mut State, options: &mut Options) {...@@ -892,7 +937,8 @@ fn renderer(state: &mut State, options: &mut Options) {
892 let labels: Vec<String> = Backend::PLATFORM.iter().copied().map(label).collect();937 let labels: Vec<String> = Backend::PLATFORM.iter().copied().map(label).collect();
893 let current = shown.map(label).unwrap_or_default();938 let current = shown.map(label).unwrap_or_default();
894 let combo = ui.id("combo");939 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);
896 let items: Vec<Item> = (Backend::PLATFORM.iter().zip(&labels))942 let items: Vec<Item> = (Backend::PLATFORM.iter().zip(&labels))
897 .map(|(backend, text)| Item {943 .map(|(backend, text)| Item {
898 text,944 text,
...@@ -904,7 +950,7 @@ fn renderer(state: &mut State, options: &mut Options) {...@@ -904,7 +950,7 @@ fn renderer(state: &mut State, options: &mut Options) {
904 })950 })
905 .collect();951 .collect();
906 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());952 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) {
908 let picked = Backend::PLATFORM[index];954 let picked = Backend::PLATFORM[index];
909 options.renderer = if Some(picked) == automatic {955 options.renderer = if Some(picked) == automatic {
910 Backend::Default956 Backend::Default
...@@ -928,36 +974,22 @@ fn renderer(state: &mut State, options: &mut Options) {...@@ -928,36 +974,22 @@ fn renderer(state: &mut State, options: &mut Options) {
928 ui.leaf("adapter", spec);974 ui.leaf("adapter", spec);
929}975}
930976
931fn lock_after(state: &mut State, options: &mut Options) {977/// Never, or one of OneNote's times, `protection::AFTER`.
932 let ui = &mut state.ui;978fn lock_after(state: &mut State, options: &mut Options, name: &str) {
933 let combo = ui.id("combo");979 let after = crate::protection::AFTER;
934 let current = crate::protection::AFTER980 let labels: Vec<&str> = std::iter::once("Never")
935 .iter()981 .chain(after.iter().map(|(_, label)| *label))
936 .find(|(minutes, _)| *minutes == options.lock_minutes)982 .collect();
937 .map_or("", |(_, name)| name);983 let chosen = options
938 ui::shell::combo(984 .lock_after
939 ui,985 .and_then(|minutes| after.iter().position(|(each, _)| *each == minutes))
940 "combo",986 .map_or(0, |index| index + 1);
941 "Amount of time",987 if let Some(index) = dropdown(&mut state.ui, name, &labels, chosen, 160.0) {
942 current,988 options.lock_after = index.checked_sub(1).map(|index| after[index].0);
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;
957 }989 }
958}990}
959991
960fn user_name_field(state: &mut State, options: &mut Options) {992fn user_name_field(state: &mut State, options: &mut Options, name: &str) {
961 let ui = &mut state.ui;993 let ui = &mut state.ui;
962 let theme = &ui.theme;994 let theme = &ui.theme;
963 let width = Extent {995 let width = Extent {
...@@ -974,53 +1006,52 @@ fn user_name_field(state: &mut State, options: &mut Options) {...@@ -974,53 +1006,52 @@ fn user_name_field(state: &mut State, options: &mut Options) {
974 };1006 };
975 ui::text_field(ui, user_name(), &mut options.user_name, "", spec);1007 ui::text_field(ui, user_name(), &mut options.user_name, "", spec);
976 if let Some(node) = ui.access(user_name()) {1008 if let Some(node) = ui.access(user_name()) {
977 node.set_label("User name");1009 node.set_label(name);
978 }1010 }
979}1011}
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) {
982 let State { ui, fonts, .. } = state;1015 let State { ui, fonts, .. } = state;
983 let font = &mut options.default_font;1016 let font = &mut options.default_font;
984 let combo = ui.id("combo");1017 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);
986 let items: Vec<_> = fonts1019 let items: Vec<_> = fonts
987 .iter()1020 .iter()
988 .map(|name| Item {1021 .map(|face| Item {
989 text: name,1022 text: face,
990 font: Some(name),1023 font: Some(face),
991 current: *name == font.face,1024 current: *face == font.face,
992 ..Item::default()1025 ..Item::default()
993 })1026 })
994 .collect();1027 .collect();
995 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());1028 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)) {
997 font.face = fonts[index].clone();1030 font.face = fonts[index].clone();
998 }1031 }
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);
1007 let labels = SIZES.map(|size| format!("{size}"));1032 let labels = SIZES.map(|size| format!("{size}"));
1008 let items: Vec<_> = labels1033 let shown = format!("{}", font.size);
1009 .iter()1034 let mut choices: Vec<&str> = labels.iter().map(String::as_str).collect();
1010 .map(|label| Item {1035 // A size typed in the toolbar's box may be none of OneNote's.
1011 text: label,1036 let chosen = SIZES.iter().position(|size| *size == font.size);
1012 checked: Some(*label == size),1037 let chosen = chosen.unwrap_or_else(|| {
1013 current: *label == size,1038 choices.push(&shown);
1014 ..Item::default()1039 choices.len() - 1
1015 })1040 });
1016 .collect();1041 ui.open(
1017 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());1042 "size",
1018 if let Some(index) = ui::popup::menu(ui, sizes(), anchor, &items, None) {1043 Spec {
1019 font.size = SIZES[index];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);
1020 }1050 }
1051 ui.close();
1021}1052}
10221053
1023fn font_color(state: &mut State, options: &mut Options) {1054fn font_color(state: &mut State, options: &mut Options, name: &str) {
1024 let ui = &mut state.ui;1055 let ui = &mut state.ui;
1025 let font = &mut options.default_font;1056 let font = &mut options.default_font;
1026 let combo = ui.id("combo");1057 let combo = ui.id("combo");
...@@ -1028,7 +1059,7 @@ fn font_color(state: &mut State, options: &mut Options) {...@@ -1028,7 +1059,7 @@ fn font_color(state: &mut State, options: &mut Options) {
1028 .iter()1059 .iter()
1029 .find(|(color, _)| Some(*color) == font.color)1060 .find(|(color, _)| Some(*color) == font.color)
1030 .map_or("Automatic", |(_, name)| name);1061 .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);
1032 let swatches: Vec<_> = crate::FONT_COLORS1063 let swatches: Vec<_> = crate::FONT_COLORS
1033 .iter()1064 .iter()
1034 .map(|&(color, name)| (canvas::gpu::colorref(color), name))1065 .map(|&(color, name)| (canvas::gpu::colorref(color), name))
...@@ -1042,31 +1073,33 @@ fn font_color(state: &mut State, options: &mut Options) {...@@ -1042,31 +1073,33 @@ fn font_color(state: &mut State, options: &mut Options) {
1042 }1073 }
1043}1074}
10441075
1045/// A path shown dimmed and cut to the row.1076/// Text shown dimmed and cut to the row, as a path.
1046fn path(ui: &mut Ui, path: &std::path::Path) {1077#[cfg(not(target_arch = "wasm32"))]
1078fn dim(ui: &mut Ui, text: &str) {
1047 let spec = Spec {1079 let spec = Spec {
1048 flags: Flags::CLIP,1080 flags: Flags::CLIP,
1049 size: [fill(), px(row_height(&ui.theme))],1081 size: [fill(), px(row_height(&ui.theme))],
1050 text: Some(&path.to_string_lossy()),1082 text: Some(text),
1051 color: Some(ui.theme.text_dim),1083 color: Some(ui.theme.text_dim),
1052 ..Spec::default()1084 ..Spec::default()
1053 };1085 };
1054 ui.leaf("path", spec);1086 ui.leaf("dim", spec);
1055}1087}
10561088
1057fn cache(state: &mut State, _: &mut Options) {1089#[cfg(not(target_arch = "wasm32"))]
1058 path(&mut state.ui, &state.cache);1090fn cache(state: &mut State, _: &mut Options, _: &str) {
1091 dim(&mut state.ui, &state.cache.to_string_lossy());
1059 let label = if cfg!(target_os = "macos") {1092 let label = if cfg!(target_os = "macos") {
1060 "Show in Finder"1093 "Show in Finder"
1061 } else {1094 } else {
1062 "Open Folder"1095 "Open Folder"
1063 };1096 };
1064 if ui::button(&mut state.ui, "reveal", label).clicked {1097 if ui::button(&mut state.ui, "reveal", label).clicked {
1065 platform::reveal(&state.cache);1098 crate::platform::reveal(&state.cache);
1066 }1099 }
1067}1100}
10681101
1069fn version(state: &mut State, _: &mut Options) {1102fn version(state: &mut State, _: &mut Options, _: &str) {
1070 let ui = &mut state.ui;1103 let ui = &mut state.ui;
1071 let spec = Spec {1104 let spec = Spec {
1072 size: [fill(), px(row_height(&ui.theme))],1105 size: [fill(), px(row_height(&ui.theme))],
...@@ -1078,20 +1111,25 @@ fn version(state: &mut State, _: &mut Options) {...@@ -1078,20 +1111,25 @@ fn version(state: &mut State, _: &mut Options) {
10781111
1079/// Where Install put Snowbound and Uninstall, or Install where it isn't in the app menu.1112/// Where Install put Snowbound and Uninstall, or Install where it isn't in the app menu.
1080#[cfg(target_os = "linux")]1113#[cfg(target_os = "linux")]
1081fn installed(state: &mut State, _: &mut Options) {1114fn installed(state: &mut State, _: &mut Options, _: &str) {
1082 let ui = &mut state.ui;1115 let ui = &mut state.ui;
1083 if crate::desktop::uninstallable() {1116 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 );
1085 if ui::button(ui, "uninstall", "Uninstall").clicked {1123 if ui::button(ui, "uninstall", "Uninstall").clicked {
1086 crate::desktop::uninstall(&state.proxy);1124 crate::desktop::uninstall(&state.proxy);
1087 }1125 }
1088 } else if crate::desktop::installable() {1126 } 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");
1090 if ui::button(ui, "install", "Install").clicked {1128 if ui::button(ui, "install", "Install").clicked {
1091 crate::desktop::install();1129 crate::desktop::install();
1092 }1130 }
1093 } else {1131 } else {
1094 path(ui, std::path::Path::new("By your system's package manager"));1132 dim(ui, "By your system's package manager");
1095 }1133 }
1096}1134}
10971135