| ... | ... | @@ -3,7 +3,7 @@ |
| 3 | 3 | //! chosen. |
| 4 | 4 | |
| 5 | 5 | use crate::{ |
| 6 | | State, UserEvent, platform, |
| 6 | State, UserEvent, |
| 7 | 7 | settings::{Backend, ColorScheme}, |
| 8 | 8 | update, |
| 9 | 9 | }; |
| ... | ... | @@ -12,15 +12,14 @@ use canvas::editor::DefaultFont; |
| 12 | 12 | use ui::{Anchor, Axis, Extent, Flags, Id, Size, Spec, Theme, Ui, fill, popup::Item, px}; |
| 13 | 13 | use winit::keyboard::NamedKey; |
| 14 | 14 | |
| 15 | | const WIDTH: f32 = 680.0; |
| 15 | const WIDTH: f32 = 700.0; |
| 16 | 16 | const NAV: f32 = 150.0; |
| 17 | | /// The column the fields' labels share. |
| 18 | | const LABEL: f32 = 104.0; |
| 19 | 17 | const SCHEMES: [(ColorScheme, &str); 3] = [ |
| 20 | 18 | (ColorScheme::System, "System"), |
| 21 | 19 | (ColorScheme::Light, "Light"), |
| 22 | 20 | (ColorScheme::Dark, "Dark"), |
| 23 | 21 | ]; |
| 22 | const SIDES: [&str; 2] = ["Left", "Right"]; |
| 24 | 23 | /// The sizes Default font offers, OneNote 2010's list in points. |
| 25 | 24 | const SIZES: [f32; 19] = [ |
| 26 | 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 | 41 | |
| 43 | 42 | struct Row { |
| 44 | 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 | 46 | /// Other words search finds the row by. |
| 46 | 47 | keywords: &'static str, |
| 47 | 48 | control: Control, |
| 48 | 49 | } |
| 49 | 50 | |
| 51 | impl 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. |
| 50 | 67 | enum 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 | 69 | 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)), |
| 55 | 73 | /// What `build` makes across the row, unlabelled, from the search's words; `finds` tells |
| 56 | 74 | /// whether they find anything in it. Words found in the row's own texts give it none. |
| 57 | 75 | Block { |
| ... | ... | @@ -60,33 +78,33 @@ enum Control { |
| 60 | 78 | }, |
| 61 | 79 | } |
| 62 | 80 | |
| 63 | | /// Every section, in the list's order. |
| 81 | /// Every section, in the list's order, with only the rows that apply here. |
| 64 | 82 | const SECTIONS: &[Section] = &[ |
| 65 | 83 | Section { |
| 66 | 84 | name: "General", |
| 67 | 85 | groups: &[ |
| 68 | 86 | Group { |
| 69 | | heading: "User Interface Options", |
| 87 | heading: "Interface", |
| 70 | 88 | 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"), |
| 81 | 100 | ], |
| 82 | 101 | }, |
| 83 | 102 | Group { |
| 84 | 103 | 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 | ], |
| 90 | 108 | }, |
| 91 | 109 | ], |
| 92 | 110 | }, |
| ... | ... | @@ -96,76 +114,68 @@ const SECTIONS: &[Section] = &[ |
| 96 | 114 | Group { |
| 97 | 115 | heading: "Default font", |
| 98 | 116 | 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 | ), |
| 114 | 127 | ], |
| 115 | 128 | }, |
| 116 | 129 | 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")], |
| 123 | 137 | }, |
| 124 | 138 | Group { |
| 125 | 139 | 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 | )], |
| 131 | 145 | }, |
| 132 | 146 | Group { |
| 133 | 147 | 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")], |
| 140 | 155 | }, |
| 141 | 156 | Group { |
| 142 | 157 | 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 | )], |
| 148 | 163 | }, |
| 149 | 164 | Group { |
| 150 | 165 | heading: "Passwords", |
| 151 | 166 | 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 | ), |
| 169 | 179 | ], |
| 170 | 180 | }, |
| 171 | 181 | ], |
| ... | ... | @@ -175,55 +185,61 @@ const SECTIONS: &[Section] = &[ |
| 175 | 185 | groups: &[Group { |
| 176 | 186 | heading: "", |
| 177 | 187 | 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 | ), |
| 194 | 208 | ], |
| 195 | 209 | }], |
| 196 | 210 | }, |
| 197 | 211 | Section { |
| 198 | 212 | name: "Sync & Storage", |
| 199 | 213 | groups: &[ |
| 214 | #[cfg(not(target_arch = "wasm32"))] |
| 200 | 215 | 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")], |
| 207 | 223 | }, |
| 208 | 224 | #[cfg(feature = "live")] |
| 209 | 225 | Group { |
| 210 | 226 | heading: "Live Share", |
| 211 | 227 | 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 | ), |
| 227 | 243 | ], |
| 228 | 244 | }, |
| 229 | 245 | ], |
| ... | ... | @@ -233,22 +249,19 @@ const SECTIONS: &[Section] = &[ |
| 233 | 249 | groups: &[Group { |
| 234 | 250 | heading: "", |
| 235 | 251 | 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 | ), |
| 246 | 259 | #[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 | ), |
| 252 | 265 | ], |
| 253 | 266 | }], |
| 254 | 267 | }, |
| ... | ... | @@ -256,14 +269,14 @@ const SECTIONS: &[Section] = &[ |
| 256 | 269 | name: "Keyboard", |
| 257 | 270 | groups: &[Group { |
| 258 | 271 | 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 { |
| 263 | 276 | build: |state, options, query| options.keyboard.build(&mut state.ui, query), |
| 264 | 277 | finds: crate::keys::searched, |
| 265 | 278 | }, |
| 266 | | }], |
| 279 | )], |
| 267 | 280 | }], |
| 268 | 281 | }, |
| 269 | 282 | ]; |
| ... | ... | @@ -283,9 +296,8 @@ pub struct Options { |
| 283 | 296 | default_font: DefaultFont, |
| 284 | 297 | page_tabs_left: bool, |
| 285 | 298 | 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>, |
| 289 | 301 | lock_on_leave: bool, |
| 290 | 302 | presence: bool, |
| 291 | 303 | picture: bool, |
| ... | ... | @@ -312,34 +324,14 @@ fn user_name() -> Id { |
| 312 | 324 | id().child("user-name") |
| 313 | 325 | } |
| 314 | 326 | |
| 315 | | fn schemes() -> Id { |
| 316 | | id().child("color-schemes") |
| 317 | | } |
| 318 | | |
| 319 | 327 | fn fonts() -> Id { |
| 320 | 328 | id().child("fonts") |
| 321 | 329 | } |
| 322 | 330 | |
| 323 | | fn sizes() -> Id { |
| 324 | | id().child("sizes") |
| 325 | | } |
| 326 | | |
| 327 | | fn lock_times() -> Id { |
| 328 | | id().child("lock-times") |
| 329 | | } |
| 330 | | |
| 331 | 331 | fn font_colors() -> Id { |
| 332 | 332 | id().child("font-colors") |
| 333 | 333 | } |
| 334 | 334 | |
| 335 | | fn notebook_themes() -> Id { |
| 336 | | id().child("notebook-themes") |
| 337 | | } |
| 338 | | |
| 339 | | fn renderers() -> Id { |
| 340 | | id().child("renderers") |
| 341 | | } |
| 342 | | |
| 343 | 335 | /// Whether `word`, lowercase, starts a word of `text`: "pen" finds "Pen" and "pen-like", |
| 344 | 336 | /// not "Open". |
| 345 | 337 | fn starts_word(text: &str, word: &str) -> bool { |
| ... | ... | @@ -394,8 +386,7 @@ impl State { |
| 394 | 386 | default_font: self.view.editor.default_font.clone(), |
| 395 | 387 | page_tabs_left: self.page_tabs_left, |
| 396 | 388 | 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, |
| 399 | 390 | lock_on_leave: self.passwords.lock_on_leave, |
| 400 | 391 | presence: self.live_options.presence, |
| 401 | 392 | picture: self.live_options.picture, |
| ... | ... | @@ -409,7 +400,8 @@ impl State { |
| 409 | 400 | } |
| 410 | 401 | |
| 411 | 402 | /// 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 | 405 | pub(crate) fn options_dialog(&mut self) { |
| 414 | 406 | let Some(mut options) = self.options.take() else { |
| 415 | 407 | return; |
| ... | ... | @@ -420,6 +412,7 @@ impl State { |
| 420 | 412 | let theme = self.ui.theme.clone(); |
| 421 | 413 | let row = row_height(&theme); |
| 422 | 414 | let ui = &mut self.ui; |
| 415 | let column = labels_column(ui); |
| 423 | 416 | let entered = ui::popup::navigation(ui, &[user_name()], &[NamedKey::Enter]) |
| 424 | 417 | .contains(&NamedKey::Enter); |
| 425 | 418 | // As tall as its contents up to the window, which the list then scrolls within. |
| ... | ... | @@ -636,28 +629,11 @@ impl State { |
| 636 | 629 | ..Spec::default() |
| 637 | 630 | }, |
| 638 | 631 | ); |
| 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); |
| 661 | 637 | } |
| 662 | 638 | self.ui.close(); |
| 663 | 639 | } |
| ... | ... | @@ -694,9 +670,11 @@ impl State { |
| 694 | 670 | let ok = ok || entered; |
| 695 | 671 | ui.close(); |
| 696 | 672 | 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 | } |
| 700 | 678 | self.color_scheme = options.color_scheme; |
| 701 | 679 | self.light_pages = !options.pages_match; |
| 702 | 680 | self.hide_spelling = options.hide_spelling; |
| ... | ... | @@ -706,7 +684,7 @@ impl State { |
| 706 | 684 | self.page_tabs_left = options.page_tabs_left; |
| 707 | 685 | self.navigation_bar_right = !options.navigation_bar_left; |
| 708 | 686 | self.passwords = crate::settings::Passwords { |
| 709 | | lock_after: options.lock_idle.then_some(options.lock_minutes), |
| 687 | lock_after: options.lock_after, |
| 710 | 688 | lock_on_leave: options.lock_on_leave, |
| 711 | 689 | }; |
| 712 | 690 | self.notebook_theme = options.notebook_theme; |
| ... | ... | @@ -734,10 +712,79 @@ impl State { |
| 734 | 712 | } |
| 735 | 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. |
| 777 | fn 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 | } |
| 738 | 785 | |
| 739 | 786 | #[cfg(feature = "live")] |
| 740 | | fn relay(state: &mut State, options: &mut Options) { |
| 787 | fn relay(state: &mut State, options: &mut Options, name: &str) { |
| 741 | 788 | let ui = &mut state.ui; |
| 742 | 789 | let theme = ui.theme.clone(); |
| 743 | 790 | let field = id().child("relay"); |
| ... | ... | @@ -756,7 +803,7 @@ fn relay(state: &mut State, options: &mut Options) { |
| 756 | 803 | }, |
| 757 | 804 | ); |
| 758 | 805 | if let Some(node) = ui.access(field) { |
| 759 | | node.set_label("Live Share relay"); |
| 806 | node.set_label(name); |
| 760 | 807 | } |
| 761 | 808 | } |
| 762 | 809 | |
| ... | ... | @@ -806,62 +853,60 @@ fn heading(ui: &mut Ui, theme: &Theme, text: &str, lit: bool) { |
| 806 | 853 | ); |
| 807 | 854 | } |
| 808 | 855 | |
| 809 | | fn 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. |
| 857 | fn dropdown(ui: &mut Ui, name: &str, choices: &[&str], chosen: usize, width: f32) -> Option<usize> { |
| 858 | let menu = id().child(name); |
| 811 | 859 | 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(); |
| 823 | 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 | |
| 874 | fn 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 | 880 | options.color_scheme = SCHEMES[index].0; |
| 826 | 881 | } |
| 827 | 882 | } |
| 828 | 883 | |
| 829 | | fn 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`. |
| 885 | fn 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 | |
| 891 | fn notebook_theme(state: &mut State, options: &mut Options, name: &str) { |
| 832 | 892 | 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()))) |
| 839 | 895 | .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())) |
| 855 | 898 | .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); |
| 859 | 904 | } |
| 860 | 905 | } |
| 861 | 906 | |
| 862 | 907 | /// The platform's backends, the one the default starts with marked, those that didn't start |
| 863 | 908 | /// this run disabled; then the backend and adapter drawing now. |
| 864 | | fn renderer(state: &mut State, options: &mut Options) { |
| 909 | fn renderer(state: &mut State, options: &mut Options, name: &str) { |
| 865 | 910 | let State { |
| 866 | 911 | ui, |
| 867 | 912 | unavailable, |
| ... | ... | @@ -892,7 +937,8 @@ fn renderer(state: &mut State, options: &mut Options) { |
| 892 | 937 | let labels: Vec<String> = Backend::PLATFORM.iter().copied().map(label).collect(); |
| 893 | 938 | let current = shown.map(label).unwrap_or_default(); |
| 894 | 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 | 942 | let items: Vec<Item> = (Backend::PLATFORM.iter().zip(&labels)) |
| 897 | 943 | .map(|(backend, text)| Item { |
| 898 | 944 | text, |
| ... | ... | @@ -904,7 +950,7 @@ fn renderer(state: &mut State, options: &mut Options) { |
| 904 | 950 | }) |
| 905 | 951 | .collect(); |
| 906 | 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 | 954 | let picked = Backend::PLATFORM[index]; |
| 909 | 955 | options.renderer = if Some(picked) == automatic { |
| 910 | 956 | Backend::Default |
| ... | ... | @@ -928,36 +974,22 @@ fn renderer(state: &mut State, options: &mut Options) { |
| 928 | 974 | ui.leaf("adapter", spec); |
| 929 | 975 | } |
| 930 | 976 | |
| 931 | | fn 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`. |
| 978 | fn 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); |
| 957 | 989 | } |
| 958 | 990 | } |
| 959 | 991 | |
| 960 | | fn user_name_field(state: &mut State, options: &mut Options) { |
| 992 | fn user_name_field(state: &mut State, options: &mut Options, name: &str) { |
| 961 | 993 | let ui = &mut state.ui; |
| 962 | 994 | let theme = &ui.theme; |
| 963 | 995 | let width = Extent { |
| ... | ... | @@ -974,53 +1006,52 @@ fn user_name_field(state: &mut State, options: &mut Options) { |
| 974 | 1006 | }; |
| 975 | 1007 | ui::text_field(ui, user_name(), &mut options.user_name, "", spec); |
| 976 | 1008 | if let Some(node) = ui.access(user_name()) { |
| 977 | | node.set_label("User name"); |
| 1009 | node.set_label(name); |
| 978 | 1010 | } |
| 979 | 1011 | } |
| 980 | 1012 | |
| 981 | | fn font_face(state: &mut State, options: &mut Options) { |
| 1013 | /// The face, from the font menu, and its size. |
| 1014 | fn font_face(state: &mut State, options: &mut Options, name: &str) { |
| 982 | 1015 | let State { ui, fonts, .. } = state; |
| 983 | 1016 | let font = &mut options.default_font; |
| 984 | 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 | 1019 | let items: Vec<_> = fonts |
| 987 | 1020 | .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, |
| 992 | 1025 | ..Item::default() |
| 993 | 1026 | }) |
| 994 | 1027 | .collect(); |
| 995 | 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 | 1030 | font.face = fonts[index].clone(); |
| 998 | 1031 | } |
| 999 | | } |
| 1000 | | |
| 1001 | | fn 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 | 1032 | 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); |
| 1020 | 1050 | } |
| 1051 | ui.close(); |
| 1021 | 1052 | } |
| 1022 | 1053 | |
| 1023 | | fn font_color(state: &mut State, options: &mut Options) { |
| 1054 | fn font_color(state: &mut State, options: &mut Options, name: &str) { |
| 1024 | 1055 | let ui = &mut state.ui; |
| 1025 | 1056 | let font = &mut options.default_font; |
| 1026 | 1057 | let combo = ui.id("combo"); |
| ... | ... | @@ -1028,7 +1059,7 @@ fn font_color(state: &mut State, options: &mut Options) { |
| 1028 | 1059 | .iter() |
| 1029 | 1060 | .find(|(color, _)| Some(*color) == font.color) |
| 1030 | 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 | 1063 | let swatches: Vec<_> = crate::FONT_COLORS |
| 1033 | 1064 | .iter() |
| 1034 | 1065 | .map(|&(color, name)| (canvas::gpu::colorref(color), name)) |
| ... | ... | @@ -1042,31 +1073,33 @@ fn font_color(state: &mut State, options: &mut Options) { |
| 1042 | 1073 | } |
| 1043 | 1074 | } |
| 1044 | 1075 | |
| 1045 | | /// A path shown dimmed and cut to the row. |
| 1046 | | fn 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"))] |
| 1078 | fn dim(ui: &mut Ui, text: &str) { |
| 1047 | 1079 | let spec = Spec { |
| 1048 | 1080 | flags: Flags::CLIP, |
| 1049 | 1081 | size: [fill(), px(row_height(&ui.theme))], |
| 1050 | | text: Some(&path.to_string_lossy()), |
| 1082 | text: Some(text), |
| 1051 | 1083 | color: Some(ui.theme.text_dim), |
| 1052 | 1084 | ..Spec::default() |
| 1053 | 1085 | }; |
| 1054 | | ui.leaf("path", spec); |
| 1086 | ui.leaf("dim", spec); |
| 1055 | 1087 | } |
| 1056 | 1088 | |
| 1057 | | fn cache(state: &mut State, _: &mut Options) { |
| 1058 | | path(&mut state.ui, &state.cache); |
| 1089 | #[cfg(not(target_arch = "wasm32"))] |
| 1090 | fn cache(state: &mut State, _: &mut Options, _: &str) { |
| 1091 | dim(&mut state.ui, &state.cache.to_string_lossy()); |
| 1059 | 1092 | let label = if cfg!(target_os = "macos") { |
| 1060 | 1093 | "Show in Finder" |
| 1061 | 1094 | } else { |
| 1062 | 1095 | "Open Folder" |
| 1063 | 1096 | }; |
| 1064 | 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 | } |
| 1068 | 1101 | |
| 1069 | | fn version(state: &mut State, _: &mut Options) { |
| 1102 | fn version(state: &mut State, _: &mut Options, _: &str) { |
| 1070 | 1103 | let ui = &mut state.ui; |
| 1071 | 1104 | let spec = Spec { |
| 1072 | 1105 | size: [fill(), px(row_height(&ui.theme))], |
| ... | ... | @@ -1078,20 +1111,25 @@ fn version(state: &mut State, _: &mut Options) { |
| 1078 | 1111 | |
| 1079 | 1112 | /// Where Install put Snowbound and Uninstall, or Install where it isn't in the app menu. |
| 1080 | 1113 | #[cfg(target_os = "linux")] |
| 1081 | | fn installed(state: &mut State, _: &mut Options) { |
| 1114 | fn installed(state: &mut State, _: &mut Options, _: &str) { |
| 1082 | 1115 | let ui = &mut state.ui; |
| 1083 | 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 | 1123 | if ui::button(ui, "uninstall", "Uninstall").clicked { |
| 1086 | 1124 | crate::desktop::uninstall(&state.proxy); |
| 1087 | 1125 | } |
| 1088 | 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 | 1128 | if ui::button(ui, "install", "Install").clicked { |
| 1091 | 1129 | crate::desktop::install(); |
| 1092 | 1130 | } |
| 1093 | 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 | } |
| 1097 | 1135 | |