| ... | ... | @@ -1,7 +1,8 @@ |
| 1 | 1 | //! Style themes in the app (`resources/styles.md`): the page wears its own theme, else its |
| 2 | 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 |
| 4 | | //! section or the notebook, and edits a notebook's own themes with a live preview. |
| 3 | //! applies a style in the page's theme, and its Customize… opens the Themes dialog, which |
| 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 | 7 | use crate::{Library, State, filetime}; |
| 7 | 8 | use accesskit::Role; |
| ... | ... | @@ -32,9 +33,9 @@ impl Scope { |
| 32 | 33 | |
| 33 | 34 | /// The Themes dialog while it is open. |
| 34 | 35 | pub struct Dialog { |
| 35 | | scope: Scope, |
| 36 | 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 | 39 | /// The built-in themes, then the notebook's own as the dialog edits them. |
| 39 | 40 | themes: Vec<Theme>, |
| 40 | 41 | /// The notebook's own themes changed here, by id. |
| ... | ... | @@ -164,40 +165,48 @@ impl State { |
| 164 | 165 | self.edit_page(ops) |
| 165 | 166 | } |
| 166 | 167 | |
| 167 | | /// Opens the Themes dialog choosing a theme for `scope` of the open page. |
| 168 | | pub(crate) fn open_themes(&mut self, scope: Scope) { |
| 169 | | let Some((library, target)) = self.theme_target(scope) else { |
| 168 | /// Opens the Themes dialog on the open page's theme, saving to the page, its section or |
| 169 | /// its notebook. |
| 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 | 180 | return; |
| 171 | 181 | }; |
| 172 | | let section = self.section_color(); |
| 173 | | self.show_themes(scope, library, target, section); |
| 182 | let library = Arc::clone(library); |
| 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 | 189 | /// section coloured `section`. |
| 178 | 190 | pub(crate) fn show_themes( |
| 179 | 191 | &mut self, |
| 180 | | scope: Scope, |
| 181 | 192 | library: Arc<Library>, |
| 182 | | target: stored::Scope, |
| 193 | targets: Vec<(Scope, stored::Scope)>, |
| 194 | wearing: Option<Theme>, |
| 183 | 195 | section: Option<u32>, |
| 184 | 196 | ) { |
| 185 | | let themes = library.themes(); |
| 186 | | let assigned = themes.assigned(&target); |
| 187 | | let mut all = themes.all(); |
| 188 | | // A retired built-in the scope wears is listed while it does. |
| 189 | | if let Some(assigned) = &assigned |
| 190 | | && !all.contains(assigned) |
| 197 | let mut all = library.themes().all(); |
| 198 | // A retired built-in worn here is listed while it is. |
| 199 | if let Some(wearing) = &wearing |
| 200 | && !all.contains(wearing) |
| 191 | 201 | { |
| 192 | | all.push(assigned.clone()); |
| 202 | all.push(wearing.clone()); |
| 193 | 203 | } |
| 194 | | let selected = assigned |
| 195 | | .and_then(|assigned| all.iter().position(|theme| theme.id == assigned.id)) |
| 204 | let selected = wearing |
| 205 | .and_then(|wearing| all.iter().position(|theme| theme.id == wearing.id)) |
| 196 | 206 | .unwrap_or(0); |
| 197 | 207 | self.themes = Some(Dialog { |
| 198 | | scope, |
| 199 | 208 | library, |
| 200 | | target, |
| 209 | targets, |
| 201 | 210 | themes: all, |
| 202 | 211 | changed: Vec::new(), |
| 203 | 212 | deleted: Vec::new(), |
| ... | ... | @@ -208,9 +217,9 @@ impl State { |
| 208 | 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 |
| 212 | | /// to the dialog's scope; No Theme takes the scope's own away; Cancel, Escape or a press |
| 213 | | /// outside leave everything as it was. |
| 220 | /// Builds the Themes dialog while it is open. Save keeps the edits and gives the theme to |
| 221 | /// the scope it then asks for, This Page first; No Theme takes that scope's own away; |
| 222 | /// Cancel, Escape or a press outside leave everything as it was. |
| 214 | 223 | pub(crate) fn themes_dialog(&mut self) { |
| 215 | 224 | let Some(dialog) = &mut self.themes else { |
| 216 | 225 | return; |
| ... | ... | @@ -241,7 +250,10 @@ impl State { |
| 241 | 250 | if let Some(node) = ui.access(id()) { |
| 242 | 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 | 257 | ui.leaf( |
| 246 | 258 | "title", |
| 247 | 259 | Spec { |
| ... | ... | @@ -568,7 +580,30 @@ impl State { |
| 568 | 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 | 607 | ui.leaf( |
| 573 | 608 | "space", |
| 574 | 609 | Spec { |
| ... | ... | @@ -577,7 +612,7 @@ impl State { |
| 577 | 612 | }, |
| 578 | 613 | ); |
| 579 | 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 | 616 | ui.close(); |
| 582 | 617 | ui.close(); |
| 583 | 618 | |
| ... | ... | @@ -617,10 +652,11 @@ impl State { |
| 617 | 652 | dialog.deleted.push(gone); |
| 618 | 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 | 657 | return; |
| 622 | 658 | } |
| 623 | | if !cancel { |
| 659 | if let Some((at, apply)) = chosen { |
| 624 | 660 | let themes = dialog |
| 625 | 661 | .themes |
| 626 | 662 | .iter() |
| ... | ... | @@ -633,7 +669,7 @@ impl State { |
| 633 | 669 | .collect(); |
| 634 | 670 | let chosen = &dialog.themes[dialog.selected]; |
| 635 | 671 | let assignment = Assignment { |
| 636 | | scope: dialog.target.clone(), |
| 672 | scope: dialog.targets[at].1.clone(), |
| 637 | 673 | theme: apply.then(|| chosen.id.clone()), |
| 638 | 674 | assigned: now, |
| 639 | 675 | }; |
| ... | ... | @@ -644,7 +680,9 @@ impl State { |
| 644 | 680 | } |
| 645 | 681 | self.ui.close_popup(id()); |
| 646 | 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 | 686 | eprintln!("Restyling the page failed: {error}"); |
| 649 | 687 | } |
| 650 | 688 | } |
| ... | ... | @@ -690,7 +728,8 @@ fn fresh_id(now: u64, count: usize) -> String { |
| 690 | 728 | } |
| 691 | 729 | |
| 692 | 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 | 733 | pub(crate) fn gallery( |
| 695 | 734 | ui: &mut Ui, |
| 696 | 735 | menu: Id, |
| ... | ... | @@ -700,7 +739,6 @@ pub(crate) fn gallery( |
| 700 | 739 | current: Option<&str>, |
| 701 | 740 | ) -> Option<crate::commands::Id> { |
| 702 | 741 | use crate::commands::Id as Cmd; |
| 703 | | let scopes = [Scope::Page, Scope::Section, Scope::Notebook]; |
| 704 | 742 | let groups = [ |
| 705 | 743 | ui::popup::Group { |
| 706 | 744 | heading: &sheet.name, |
| ... | ... | @@ -709,8 +747,8 @@ pub(crate) fn gallery( |
| 709 | 747 | size: [240.0, 34.0], |
| 710 | 748 | }, |
| 711 | 749 | ui::popup::Group { |
| 712 | | heading: "Theme", |
| 713 | | cells: scopes.len(), |
| 750 | heading: "", |
| 751 | cells: 1, |
| 714 | 752 | columns: 1, |
| 715 | 753 | size: [240.0, 24.0], |
| 716 | 754 | }, |
| ... | ... | @@ -730,20 +768,20 @@ pub(crate) fn gallery( |
| 730 | 768 | } |
| 731 | 769 | } |
| 732 | 770 | None => { |
| 733 | | let label = format!("{}…", scopes[index - STYLES.len()].name()); |
| 734 | 771 | ui.leaf( |
| 735 | | "scope", |
| 772 | "customize", |
| 736 | 773 | Spec { |
| 737 | 774 | size: [fill(), fill()], |
| 738 | | text: Some(&label), |
| 775 | text: Some("Customize…"), |
| 739 | 776 | ..Spec::default() |
| 740 | 777 | }, |
| 741 | 778 | ); |
| 742 | 779 | } |
| 743 | 780 | })?; |
| 744 | | Some(match scopes.get(chosen.wrapping_sub(STYLES.len())) { |
| 745 | | Some(scope) => Cmd::Theme(*scope), |
| 746 | | None => Cmd::Style(chosen), |
| 781 | Some(if chosen < STYLES.len() { |
| 782 | Cmd::Style(chosen) |
| 783 | } else { |
| 784 | Cmd::Themes |
| 747 | 785 | }) |
| 748 | 786 | } |
| 749 | 787 | |