| ... | @@ -1,7 +1,8 @@ | ... | @@ -1,7 +1,8 @@ |
| 1 | //! Style themes in the app (`resources/styles.md`): the page wears its own theme, else its | 1 | //! Style themes in the app (`resources/styles.md`): the page wears its own theme, else its |
| 2 | //! section's, else its notebook's, as its paragraph style objects; the Styles gallery | 2 | //! section's, else its notebook's, as its paragraph style objects; the Styles gallery |
| 3 | //! applies a style in the page's theme; the Themes dialog picks a theme for the page, the | 3 | //! applies a style in the page's theme, and its Customize… opens the Themes dialog, which |
| 4 | //! section or the notebook, and edits a notebook's own themes with a live preview. | 4 | //! edits a notebook's own themes with a live preview and gives one to the page, the section |
| | 5 | //! or the notebook. |
| 5 | | 6 | |
| 6 | use crate::{Library, State, filetime}; | 7 | use crate::{Library, State, filetime}; |
| 7 | use accesskit::Role; | 8 | use accesskit::Role; |
| ... | @@ -32,9 +33,9 @@ impl Scope { | ... | @@ -32,9 +33,9 @@ impl Scope { |
| 32 | | 33 | |
| 33 | /// The Themes dialog while it is open. | 34 | /// The Themes dialog while it is open. |
| 34 | pub struct Dialog { | 35 | pub struct Dialog { |
| 35 | scope: Scope, | | |
| 36 | library: Arc<Library>, | 36 | library: Arc<Library>, |
| 37 | target: stored::Scope, | 37 | /// Where Save can give the theme, asked with the first offered first; one alone takes it. |
| | 38 | targets: Vec<(Scope, stored::Scope)>, |
| 38 | /// The built-in themes, then the notebook's own as the dialog edits them. | 39 | /// The built-in themes, then the notebook's own as the dialog edits them. |
| 39 | themes: Vec<Theme>, | 40 | themes: Vec<Theme>, |
| 40 | /// The notebook's own themes changed here, by id. | 41 | /// The notebook's own themes changed here, by id. |
| ... | @@ -164,40 +165,48 @@ impl State { | ... | @@ -164,40 +165,48 @@ impl State { |
| 164 | self.edit_page(ops) | 165 | self.edit_page(ops) |
| 165 | } | 166 | } |
| 166 | | 167 | |
| 167 | /// Opens the Themes dialog choosing a theme for `scope` of the open page. | 168 | /// Opens the Themes dialog on the open page's theme, saving to the page, its section or |
| 168 | pub(crate) fn open_themes(&mut self, scope: Scope) { | 169 | /// its notebook. |
| 169 | let Some((library, target)) = self.theme_target(scope) else { | 170 | pub(crate) fn open_themes(&mut self) { |
| | 171 | let targets: Vec<(Arc<Library>, (Scope, stored::Scope))> = |
| | 172 | [Scope::Page, Scope::Section, Scope::Notebook] |
| | 173 | .into_iter() |
| | 174 | .filter_map(|scope| { |
| | 175 | let (library, target) = self.theme_target(scope)?; |
| | 176 | Some((library, (scope, target))) |
| | 177 | }) |
| | 178 | .collect(); |
| | 179 | let Some((library, _)) = targets.first() else { |
| 170 | return; | 180 | return; |
| 171 | }; | 181 | }; |
| 172 | let section = self.section_color(); | 182 | let library = Arc::clone(library); |
| 173 | self.show_themes(scope, library, target, section); | 183 | let (wearing, section) = (self.page_theme(), self.section_color()); |
| | 184 | let targets = targets.into_iter().map(|(_, target)| target).collect(); |
| | 185 | self.show_themes(library, targets, wearing, section); |
| 174 | } | 186 | } |
| 175 | | 187 | |
| 176 | /// Opens the Themes dialog choosing a theme for `target`, which `scope` names, in a | 188 | /// Opens the Themes dialog on theme `wearing`, saving to `targets` of `library`, in a |
| 177 | /// section coloured `section`. | 189 | /// section coloured `section`. |
| 178 | pub(crate) fn show_themes( | 190 | pub(crate) fn show_themes( |
| 179 | &mut self, | 191 | &mut self, |
| 180 | scope: Scope, | | |
| 181 | library: Arc<Library>, | 192 | library: Arc<Library>, |
| 182 | target: stored::Scope, | 193 | targets: Vec<(Scope, stored::Scope)>, |
| | 194 | wearing: Option<Theme>, |
| 183 | section: Option<u32>, | 195 | section: Option<u32>, |
| 184 | ) { | 196 | ) { |
| 185 | let themes = library.themes(); | 197 | let mut all = library.themes().all(); |
| 186 | let assigned = themes.assigned(&target); | 198 | // A retired built-in worn here is listed while it is. |
| 187 | let mut all = themes.all(); | 199 | if let Some(wearing) = &wearing |
| 188 | // A retired built-in the scope wears is listed while it does. | 200 | && !all.contains(wearing) |
| 189 | if let Some(assigned) = &assigned | | |
| 190 | && !all.contains(assigned) | | |
| 191 | { | 201 | { |
| 192 | all.push(assigned.clone()); | 202 | all.push(wearing.clone()); |
| 193 | } | 203 | } |
| 194 | let selected = assigned | 204 | let selected = wearing |
| 195 | .and_then(|assigned| all.iter().position(|theme| theme.id == assigned.id)) | 205 | .and_then(|wearing| all.iter().position(|theme| theme.id == wearing.id)) |
| 196 | .unwrap_or(0); | 206 | .unwrap_or(0); |
| 197 | self.themes = Some(Dialog { | 207 | self.themes = Some(Dialog { |
| 198 | scope, | | |
| 199 | library, | 208 | library, |
| 200 | target, | 209 | targets, |
| 201 | themes: all, | 210 | themes: all, |
| 202 | changed: Vec::new(), | 211 | changed: Vec::new(), |
| 203 | deleted: Vec::new(), | 212 | deleted: Vec::new(), |
| ... | @@ -208,9 +217,9 @@ impl State { | ... | @@ -208,9 +217,9 @@ impl State { |
| 208 | self.ui.open_popup(id()); | 217 | self.ui.open_popup(id()); |
| 209 | } | 218 | } |
| 210 | | 219 | |
| 211 | /// Builds the Themes dialog while it is open. Apply keeps the edits and gives the theme | 220 | /// Builds the Themes dialog while it is open. Save keeps the edits and gives the theme to |
| 212 | /// to the dialog's scope; No Theme takes the scope's own away; Cancel, Escape or a press | 221 | /// the scope it then asks for, This Page first; No Theme takes that scope's own away; |
| 213 | /// outside leave everything as it was. | 222 | /// Cancel, Escape or a press outside leave everything as it was. |
| 214 | pub(crate) fn themes_dialog(&mut self) { | 223 | pub(crate) fn themes_dialog(&mut self) { |
| 215 | let Some(dialog) = &mut self.themes else { | 224 | let Some(dialog) = &mut self.themes else { |
| 216 | return; | 225 | return; |
| ... | @@ -241,7 +250,10 @@ impl State { | ... | @@ -241,7 +250,10 @@ impl State { |
| 241 | if let Some(node) = ui.access(id()) { | 250 | if let Some(node) = ui.access(id()) { |
| 242 | node.set_label("Themes"); | 251 | node.set_label("Themes"); |
| 243 | } | 252 | } |
| 244 | let title = format!("Theme for {}", dialog.scope.name()); | 253 | let title = match &dialog.targets[..] { |
| | 254 | [(scope, _)] => format!("Theme for {}", scope.name()), |
| | 255 | _ => "Themes".to_owned(), |
| | 256 | }; |
| 245 | ui.leaf( | 257 | ui.leaf( |
| 246 | "title", | 258 | "title", |
| 247 | Spec { | 259 | Spec { |
| ... | @@ -568,7 +580,30 @@ impl State { | ... | @@ -568,7 +580,30 @@ impl State { |
| 568 | ..Spec::default() | 580 | ..Spec::default() |
| 569 | }, | 581 | }, |
| 570 | ); | 582 | ); |
| 571 | let none = ui::button(ui, "none", "No Theme").clicked; | 583 | // Each button gives its choice to the one place there is, else asks where. |
| | 584 | let targets = &dialog.targets; |
| | 585 | let place = |ui: &mut Ui, part: &str, label: &str| -> Option<usize> { |
| | 586 | let clicked = ui::button(ui, part, label).clicked; |
| | 587 | if targets.len() == 1 { |
| | 588 | return clicked.then_some(0); |
| | 589 | } |
| | 590 | let menu = popup(part); |
| | 591 | if clicked { |
| | 592 | ui.open_popup(menu); |
| | 593 | } |
| | 594 | let items: Vec<Item> = targets |
| | 595 | .iter() |
| | 596 | .enumerate() |
| | 597 | .map(|(at, (scope, _))| Item { |
| | 598 | text: scope.name(), |
| | 599 | current: at == 0, |
| | 600 | ..Item::default() |
| | 601 | }) |
| | 602 | .collect(); |
| | 603 | let anchor = Anchor::Below(ui.rect(ui.id(part)).unwrap_or_default()); |
| | 604 | ui::popup::menu(ui, menu, anchor, &items, None) |
| | 605 | }; |
| | 606 | let none = place(ui, "none", "No Theme"); |
| 572 | ui.leaf( | 607 | ui.leaf( |
| 573 | "space", | 608 | "space", |
| 574 | Spec { | 609 | Spec { |
| ... | @@ -577,7 +612,7 @@ impl State { | ... | @@ -577,7 +612,7 @@ impl State { |
| 577 | }, | 612 | }, |
| 578 | ); | 613 | ); |
| 579 | let cancel = ui::button(ui, "cancel", "Cancel").clicked; | 614 | let cancel = ui::button(ui, "cancel", "Cancel").clicked; |
| 580 | let apply = ui::button(ui, "apply", "Apply").clicked; | 615 | let save = place(ui, "save", "Save"); |
| 581 | ui.close(); | 616 | ui.close(); |
| 582 | ui.close(); | 617 | ui.close(); |
| 583 | | 618 | |
| ... | @@ -617,10 +652,11 @@ impl State { | ... | @@ -617,10 +652,11 @@ impl State { |
| 617 | dialog.deleted.push(gone); | 652 | dialog.deleted.push(gone); |
| 618 | dialog.selected = 0; | 653 | dialog.selected = 0; |
| 619 | } | 654 | } |
| 620 | if !(apply || none || cancel) { | 655 | let chosen = save.map(|at| (at, true)).or(none.map(|at| (at, false))); |
| | 656 | if chosen.is_none() && !cancel { |
| 621 | return; | 657 | return; |
| 622 | } | 658 | } |
| 623 | if !cancel { | 659 | if let Some((at, apply)) = chosen { |
| 624 | let themes = dialog | 660 | let themes = dialog |
| 625 | .themes | 661 | .themes |
| 626 | .iter() | 662 | .iter() |
| ... | @@ -633,7 +669,7 @@ impl State { | ... | @@ -633,7 +669,7 @@ impl State { |
| 633 | .collect(); | 669 | .collect(); |
| 634 | let chosen = &dialog.themes[dialog.selected]; | 670 | let chosen = &dialog.themes[dialog.selected]; |
| 635 | let assignment = Assignment { | 671 | let assignment = Assignment { |
| 636 | scope: dialog.target.clone(), | 672 | scope: dialog.targets[at].1.clone(), |
| 637 | theme: apply.then(|| chosen.id.clone()), | 673 | theme: apply.then(|| chosen.id.clone()), |
| 638 | assigned: now, | 674 | assigned: now, |
| 639 | }; | 675 | }; |
| ... | @@ -644,7 +680,9 @@ impl State { | ... | @@ -644,7 +680,9 @@ impl State { |
| 644 | } | 680 | } |
| 645 | self.ui.close_popup(id()); | 681 | self.ui.close_popup(id()); |
| 646 | self.themes = None; | 682 | self.themes = None; |
| 647 | if !cancel && let Err(error) = self.wear_theme() { | 683 | if chosen.is_some() |
| | 684 | && let Err(error) = self.wear_theme() |
| | 685 | { |
| 648 | eprintln!("Restyling the page failed: {error}"); | 686 | eprintln!("Restyling the page failed: {error}"); |
| 649 | } | 687 | } |
| 650 | } | 688 | } |
| ... | @@ -690,7 +728,8 @@ fn fresh_id(now: u64, count: usize) -> String { | ... | @@ -690,7 +728,8 @@ fn fresh_id(now: u64, count: usize) -> String { |
| 690 | } | 728 | } |
| 691 | | 729 | |
| 692 | /// The Styles gallery under the toolbar's Styles button: the eleven styles drawn in | 730 | /// The Styles gallery under the toolbar's Styles button: the eleven styles drawn in |
| 693 | /// `sheet` in a section coloured `section`, the one at the caret outlined, then the theme commands. Returns the command chosen. | 731 | /// `sheet` in a section coloured `section`, the one at the caret outlined, then Customize…. |
| | 732 | /// Returns the command chosen. |
| 694 | pub(crate) fn gallery( | 733 | pub(crate) fn gallery( |
| 695 | ui: &mut Ui, | 734 | ui: &mut Ui, |
| 696 | menu: Id, | 735 | menu: Id, |
| ... | @@ -700,7 +739,6 @@ pub(crate) fn gallery( | ... | @@ -700,7 +739,6 @@ pub(crate) fn gallery( |
| 700 | current: Option<&str>, | 739 | current: Option<&str>, |
| 701 | ) -> Option<crate::commands::Id> { | 740 | ) -> Option<crate::commands::Id> { |
| 702 | use crate::commands::Id as Cmd; | 741 | use crate::commands::Id as Cmd; |
| 703 | let scopes = [Scope::Page, Scope::Section, Scope::Notebook]; | | |
| 704 | let groups = [ | 742 | let groups = [ |
| 705 | ui::popup::Group { | 743 | ui::popup::Group { |
| 706 | heading: &sheet.name, | 744 | heading: &sheet.name, |
| ... | @@ -709,8 +747,8 @@ pub(crate) fn gallery( | ... | @@ -709,8 +747,8 @@ pub(crate) fn gallery( |
| 709 | size: [240.0, 34.0], | 747 | size: [240.0, 34.0], |
| 710 | }, | 748 | }, |
| 711 | ui::popup::Group { | 749 | ui::popup::Group { |
| 712 | heading: "Theme", | 750 | heading: "", |
| 713 | cells: scopes.len(), | 751 | cells: 1, |
| 714 | columns: 1, | 752 | columns: 1, |
| 715 | size: [240.0, 24.0], | 753 | size: [240.0, 24.0], |
| 716 | }, | 754 | }, |
| ... | @@ -730,20 +768,20 @@ pub(crate) fn gallery( | ... | @@ -730,20 +768,20 @@ pub(crate) fn gallery( |
| 730 | } | 768 | } |
| 731 | } | 769 | } |
| 732 | None => { | 770 | None => { |
| 733 | let label = format!("{}…", scopes[index - STYLES.len()].name()); | | |
| 734 | ui.leaf( | 771 | ui.leaf( |
| 735 | "scope", | 772 | "customize", |
| 736 | Spec { | 773 | Spec { |
| 737 | size: [fill(), fill()], | 774 | size: [fill(), fill()], |
| 738 | text: Some(&label), | 775 | text: Some("Customize…"), |
| 739 | ..Spec::default() | 776 | ..Spec::default() |
| 740 | }, | 777 | }, |
| 741 | ); | 778 | ); |
| 742 | } | 779 | } |
| 743 | })?; | 780 | })?; |
| 744 | Some(match scopes.get(chosen.wrapping_sub(STYLES.len())) { | 781 | Some(if chosen < STYLES.len() { |
| 745 | Some(scope) => Cmd::Theme(*scope), | 782 | Cmd::Style(chosen) |
| 746 | None => Cmd::Style(chosen), | 783 | } else { |
| | 784 | Cmd::Themes |
| 747 | }) | 785 | }) |
| 748 | } | 786 | } |
| 749 | | 787 | |