From 8bfedcad86c706463601020a107b0cad26ad75ae Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 17:52:36 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20the=20Styles=20gallery=20ends=20in=20Cu?= =?UTF-8?q?stomize=E2=80=A6,=20and=20saving=20a=20theme=20asks=20where=20i?= =?UTF-8?q?t=20goes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gallery's This Page…, This Section… and This Notebook… become one Customize…, as Format's Theme menu becomes Customize Theme…. The Themes dialog's Save and No Theme ask This Page, This Section or This Notebook, This Page first; opened from a tab's or the notebook's Theme… they apply there at once. Assisted-by: claude-opus-5.5 --- crates/snowbound/src/commands.rs | 32 +++----- crates/snowbound/src/menubar.rs | 9 +-- crates/snowbound/src/menus.rs | 17 +++-- crates/snowbound/src/themes.rs | 124 ++++++++++++++++++++----------- 4 files changed, 103 insertions(+), 79 deletions(-) diff --git a/crates/snowbound/src/commands.rs b/crates/snowbound/src/commands.rs index 60c44f588b294105f00333c88ed30b9d200cb028..7e7ba93609dd97e7b2945770019649fac29663da 100644 --- a/crates/snowbound/src/commands.rs +++ b/crates/snowbound/src/commands.rs @@ -112,8 +112,9 @@ pub enum Id { Style(usize), /// The Styles gallery. Styles, - /// The Themes dialog, assigning a theme to the page, its section or its notebook. - Theme(crate::themes::Scope), + /// The Themes dialog, editing themes and giving one to the page, its section or its + /// notebook. + Themes, /// The tag at this place in the user's tag list. Tag(usize), CustomizeTags, @@ -595,24 +596,7 @@ pub const COMMANDS: &[Command] = &[ row!(Id::Style(8), "Quote", NONE, NONE), row!(Id::Style(9), "Code", NONE, NONE), row!(Id::Style(10), "Normal", NONE, NONE), - row!( - Id::Theme(crate::themes::Scope::Page), - "Page Theme…", - NONE, - NONE - ), - row!( - Id::Theme(crate::themes::Scope::Section), - "Section Theme…", - NONE, - NONE - ), - row!( - Id::Theme(crate::themes::Scope::Notebook), - "Notebook Theme…", - NONE, - NONE - ), + row!(Id::Themes, "Customize Theme…", NONE, NONE), row!(Id::CustomizeTags, "Customize Tags…", NONE, NONE), row!( Id::RemoveTags, @@ -1349,7 +1333,9 @@ impl State { .get(place) .map(|(name, _)| *name), ), - Id::Theme(scope) => enabled(!modal && self.theme_target(scope).is_some()), + Id::Themes => { + enabled(!modal && self.theme_target(crate::themes::Scope::Notebook).is_some()) + } // A selected picture or file takes tags too, as in OneNote. Id::RemoveTags => enabled(text || tagged), Id::ToDoList => enabled(text && format.listed && self.to_do().is_some()), @@ -1761,8 +1747,8 @@ impl State { self.ui.open_popup(crate::toolbar_popup("styles")); return Ok(()); } - Id::Theme(scope) => { - self.open_themes(scope); + Id::Themes => { + self.open_themes(); return Ok(()); } Id::Tag(place) => { diff --git a/crates/snowbound/src/menubar.rs b/crates/snowbound/src/menubar.rs index d44418043cd92602d7109d7e216354132b70b85b..e03f4cb562b66dee1d4af0faa4a3b7c672d4158d 100644 --- a/crates/snowbound/src/menubar.rs +++ b/crates/snowbound/src/menubar.rs @@ -187,14 +187,7 @@ const MENUS: &[Item] = &[ C(Id::Style(10)), ], ), - Item::Menu( - "Theme", - &[ - C(Id::Theme(crate::themes::Scope::Page)), - C(Id::Theme(crate::themes::Scope::Section)), - C(Id::Theme(crate::themes::Scope::Notebook)), - ], - ), + C(Id::Themes), S, Item::Menu("Font", &[Item::Fonts]), Item::Menu("Size", &[Item::Sizes]), diff --git a/crates/snowbound/src/menus.rs b/crates/snowbound/src/menus.rs index 5b22e86dc0c59089c137281ee709ba9b3afaa992..07cc5337978dbfdfa739226fb0c961bfdac599fa 100644 --- a/crates/snowbound/src/menus.rs +++ b/crates/snowbound/src/menus.rs @@ -709,14 +709,20 @@ impl State { (Target::Section { library, path }, Action::Theme) => { if let Some(identity) = library.section_identity(&path) { let scope = notebook::sidecar::themes::Scope::section(identity); - let color = library.section_color(&path); - self.show_themes(crate::themes::Scope::Section, library, scope, color); + let (wearing, color) = ( + library.themes().assigned(&scope), + library.section_color(&path), + ); + let targets = vec![(crate::themes::Scope::Section, scope)]; + self.show_themes(library, targets, wearing, color); } None } (Target::Notebook(library), Action::Theme) => { let scope = notebook::sidecar::themes::Scope::Notebook; - self.show_themes(crate::themes::Scope::Notebook, library, scope, None); + let wearing = library.themes().assigned(&scope); + let targets = vec![(crate::themes::Scope::Notebook, scope)]; + self.show_themes(library, targets, wearing, None); None } (Target::Section { library, path }, Action::Color(color)) => Some(Command::Structure( @@ -844,8 +850,9 @@ impl State { let identity = session.section.page(space).ok()?.identity?; let library = Arc::clone(&session.library); let scope = notebook::sidecar::themes::Scope::page(identity); - let color = self.section_color(); - self.show_themes(crate::themes::Scope::Page, library, scope, color); + let (wearing, color) = (library.themes().assigned(&scope), self.section_color()); + let targets = vec![(crate::themes::Scope::Page, scope)]; + self.show_themes(library, targets, wearing, color); None } Action::Versions(show) => Some(Command::History { page: space, show }), diff --git a/crates/snowbound/src/themes.rs b/crates/snowbound/src/themes.rs index 5cbd818d42c4a6635b636eaea2f346777b27fa79..a15751acb1e92e5f716ad5f704baadf74301ddfe 100644 --- a/crates/snowbound/src/themes.rs +++ b/crates/snowbound/src/themes.rs @@ -1,7 +1,8 @@ //! Style themes in the app (`resources/styles.md`): the page wears its own theme, else its //! section's, else its notebook's, as its paragraph style objects; the Styles gallery -//! applies a style in the page's theme; the Themes dialog picks a theme for the page, the -//! section or the notebook, and edits a notebook's own themes with a live preview. +//! applies a style in the page's theme, and its Customize… opens the Themes dialog, which +//! edits a notebook's own themes with a live preview and gives one to the page, the section +//! or the notebook. use crate::{Library, State, filetime}; use accesskit::Role; @@ -32,9 +33,9 @@ impl Scope { /// The Themes dialog while it is open. pub struct Dialog { - scope: Scope, library: Arc, - target: stored::Scope, + /// Where Save can give the theme, asked with the first offered first; one alone takes it. + targets: Vec<(Scope, stored::Scope)>, /// The built-in themes, then the notebook's own as the dialog edits them. themes: Vec, /// The notebook's own themes changed here, by id. @@ -164,40 +165,48 @@ impl State { self.edit_page(ops) } - /// Opens the Themes dialog choosing a theme for `scope` of the open page. - pub(crate) fn open_themes(&mut self, scope: Scope) { - let Some((library, target)) = self.theme_target(scope) else { + /// Opens the Themes dialog on the open page's theme, saving to the page, its section or + /// its notebook. + pub(crate) fn open_themes(&mut self) { + let targets: Vec<(Arc, (Scope, stored::Scope))> = + [Scope::Page, Scope::Section, Scope::Notebook] + .into_iter() + .filter_map(|scope| { + let (library, target) = self.theme_target(scope)?; + Some((library, (scope, target))) + }) + .collect(); + let Some((library, _)) = targets.first() else { return; }; - let section = self.section_color(); - self.show_themes(scope, library, target, section); + let library = Arc::clone(library); + let (wearing, section) = (self.page_theme(), self.section_color()); + let targets = targets.into_iter().map(|(_, target)| target).collect(); + self.show_themes(library, targets, wearing, section); } - /// Opens the Themes dialog choosing a theme for `target`, which `scope` names, in a + /// Opens the Themes dialog on theme `wearing`, saving to `targets` of `library`, in a /// section coloured `section`. pub(crate) fn show_themes( &mut self, - scope: Scope, library: Arc, - target: stored::Scope, + targets: Vec<(Scope, stored::Scope)>, + wearing: Option, section: Option, ) { - let themes = library.themes(); - let assigned = themes.assigned(&target); - let mut all = themes.all(); - // A retired built-in the scope wears is listed while it does. - if let Some(assigned) = &assigned - && !all.contains(assigned) + let mut all = library.themes().all(); + // A retired built-in worn here is listed while it is. + if let Some(wearing) = &wearing + && !all.contains(wearing) { - all.push(assigned.clone()); + all.push(wearing.clone()); } - let selected = assigned - .and_then(|assigned| all.iter().position(|theme| theme.id == assigned.id)) + let selected = wearing + .and_then(|wearing| all.iter().position(|theme| theme.id == wearing.id)) .unwrap_or(0); self.themes = Some(Dialog { - scope, library, - target, + targets, themes: all, changed: Vec::new(), deleted: Vec::new(), @@ -208,9 +217,9 @@ impl State { self.ui.open_popup(id()); } - /// Builds the Themes dialog while it is open. Apply keeps the edits and gives the theme - /// to the dialog's scope; No Theme takes the scope's own away; Cancel, Escape or a press - /// outside leave everything as it was. + /// Builds the Themes dialog while it is open. Save keeps the edits and gives the theme to + /// the scope it then asks for, This Page first; No Theme takes that scope's own away; + /// Cancel, Escape or a press outside leave everything as it was. pub(crate) fn themes_dialog(&mut self) { let Some(dialog) = &mut self.themes else { return; @@ -241,7 +250,10 @@ impl State { if let Some(node) = ui.access(id()) { node.set_label("Themes"); } - let title = format!("Theme for {}", dialog.scope.name()); + let title = match &dialog.targets[..] { + [(scope, _)] => format!("Theme for {}", scope.name()), + _ => "Themes".to_owned(), + }; ui.leaf( "title", Spec { @@ -568,7 +580,30 @@ impl State { ..Spec::default() }, ); - let none = ui::button(ui, "none", "No Theme").clicked; + // Each button gives its choice to the one place there is, else asks where. + let targets = &dialog.targets; + let place = |ui: &mut Ui, part: &str, label: &str| -> Option { + let clicked = ui::button(ui, part, label).clicked; + if targets.len() == 1 { + return clicked.then_some(0); + } + let menu = popup(part); + if clicked { + ui.open_popup(menu); + } + let items: Vec = targets + .iter() + .enumerate() + .map(|(at, (scope, _))| Item { + text: scope.name(), + current: at == 0, + ..Item::default() + }) + .collect(); + let anchor = Anchor::Below(ui.rect(ui.id(part)).unwrap_or_default()); + ui::popup::menu(ui, menu, anchor, &items, None) + }; + let none = place(ui, "none", "No Theme"); ui.leaf( "space", Spec { @@ -577,7 +612,7 @@ impl State { }, ); let cancel = ui::button(ui, "cancel", "Cancel").clicked; - let apply = ui::button(ui, "apply", "Apply").clicked; + let save = place(ui, "save", "Save"); ui.close(); ui.close(); @@ -617,10 +652,11 @@ impl State { dialog.deleted.push(gone); dialog.selected = 0; } - if !(apply || none || cancel) { + let chosen = save.map(|at| (at, true)).or(none.map(|at| (at, false))); + if chosen.is_none() && !cancel { return; } - if !cancel { + if let Some((at, apply)) = chosen { let themes = dialog .themes .iter() @@ -633,7 +669,7 @@ impl State { .collect(); let chosen = &dialog.themes[dialog.selected]; let assignment = Assignment { - scope: dialog.target.clone(), + scope: dialog.targets[at].1.clone(), theme: apply.then(|| chosen.id.clone()), assigned: now, }; @@ -644,7 +680,9 @@ impl State { } self.ui.close_popup(id()); self.themes = None; - if !cancel && let Err(error) = self.wear_theme() { + if chosen.is_some() + && let Err(error) = self.wear_theme() + { eprintln!("Restyling the page failed: {error}"); } } @@ -690,7 +728,8 @@ fn fresh_id(now: u64, count: usize) -> String { } /// The Styles gallery under the toolbar's Styles button: the eleven styles drawn in -/// `sheet` in a section coloured `section`, the one at the caret outlined, then the theme commands. Returns the command chosen. +/// `sheet` in a section coloured `section`, the one at the caret outlined, then Customize…. +/// Returns the command chosen. pub(crate) fn gallery( ui: &mut Ui, menu: Id, @@ -700,7 +739,6 @@ pub(crate) fn gallery( current: Option<&str>, ) -> Option { use crate::commands::Id as Cmd; - let scopes = [Scope::Page, Scope::Section, Scope::Notebook]; let groups = [ ui::popup::Group { heading: &sheet.name, @@ -709,8 +747,8 @@ pub(crate) fn gallery( size: [240.0, 34.0], }, ui::popup::Group { - heading: "Theme", - cells: scopes.len(), + heading: "", + cells: 1, columns: 1, size: [240.0, 24.0], }, @@ -730,20 +768,20 @@ pub(crate) fn gallery( } } None => { - let label = format!("{}…", scopes[index - STYLES.len()].name()); ui.leaf( - "scope", + "customize", Spec { size: [fill(), fill()], - text: Some(&label), + text: Some("Customize…"), ..Spec::default() }, ); } })?; - Some(match scopes.get(chosen.wrapping_sub(STYLES.len())) { - Some(scope) => Cmd::Theme(*scope), - None => Cmd::Style(chosen), + Some(if chosen < STYLES.len() { + Cmd::Style(chosen) + } else { + Cmd::Themes }) } -- 2.54.0