| author | |
| committer | |
| log | f729362858ad787f269bc50144f0c765a84b366f |
| tree | fcc7a17f132f966130fbe9932e076cb4f3f7ec6c |
| parent | 292bb9cf4d5f1dc50b873b5eb9a3c46b562a57fe |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Assisted-by: claude-opus-5.510 files changed, 317 insertions(+), 288 deletions(-)
arc/ui.md+2-1| ... | @@ -27,7 +27,8 @@ it is, and it settles fast. Popups open and close on short timed curves, | ... | @@ -27,7 +27,8 @@ it is, and it settles fast. Popups open and close on short timed curves, |
| 27 | slow enough to follow: a menu swings out of the pointer or its button as it | 27 | slow enough to follow: a menu swings out of the pointer or its button as it |
| 28 | fades in, a combo's field widens into its list, and a dialog swings up into | 28 | fades in, a combo's field widens into its list, and a dialog swings up into |
| 29 | place near the window's top over a dimmed window, as Windows opens a window; the | 29 | place near the window's top over a dimmed window, as Windows opens a window; the |
| 30 | command palette swings in the same way, undimmed. A filtered list shows its new | 30 | command palette swings in the same way, undimmed, as does a dialog docked inside the |
| 31 | page it changes, such as the backgrounds gallery. A filtered list shows its new | ||
| 31 | results at once. On GNOME and KDE, menus instead | 32 | results at once. On GNOME and KDE, menus instead |
| 32 | look and move as the desktop's own (see [platforms](platforms.md)). A segmented control | 33 | look and move as the desktop's own (see [platforms](platforms.md)). A segmented control |
| 33 | slides a raised face along a track, as macOS, libadwaita and Windows 11 draw theirs, and on | 34 | slides a raised face along a track, as macOS, libadwaita and Windows 11 draw theirs, and on |
crates/snowbound/src/background.rs+16-6| ... | @@ -4,7 +4,7 @@ | ... | @@ -4,7 +4,7 @@ |
| 4 | use crate::{ | 4 | use crate::{ |
| 5 | art, | 5 | art, |
| 6 | commands::Choice, | 6 | commands::Choice, |
| 7 | templates::{TILE, Thumbnails, View}, | 7 | templates::{TILE, Thumbnails}, |
| 8 | }; | 8 | }; |
| 9 | use canvas::{ | 9 | use canvas::{ |
| 10 | gpu::{Paper, colorref}, | 10 | gpu::{Paper, colorref}, |
| ... | @@ -42,7 +42,8 @@ enum Cell { | ... | @@ -42,7 +42,8 @@ enum Cell { |
| 42 | 42 | ||
| 43 | /// Builds popup `id` beside `anchor` while it is open, for a page on `paper` (its own colour | 43 | /// Builds popup `id` beside `anchor` while it is open, for a page on `paper` (its own colour |
| 44 | /// aside) coloured `color` and ruled with `rule_lines`, and the custom colour picker it | 44 | /// aside) coloured `color` and ruled with `rule_lines`, and the custom colour picker it |
| 45 | /// opens. Show All opens every template's art over the page through `view`. Returns the | 45 | /// opens, or with `colors_only` only its page colours. Show All opens the backgrounds |
| 46 | /// gallery. Sets `preview` to the page colour highlighted or being picked. Returns the | ||
| 46 | /// choice made. | 47 | /// choice made. |
| 47 | #[allow(clippy::too_many_arguments)] | 48 | #[allow(clippy::too_many_arguments)] |
| 48 | pub fn menu( | 49 | pub fn menu( |
| ... | @@ -50,10 +51,11 @@ pub fn menu( | ... | @@ -50,10 +51,11 @@ pub fn menu( |
| 50 | id: Id, | 51 | id: Id, |
| 51 | anchor: Anchor, | 52 | anchor: Anchor, |
| 52 | thumbnails: &mut Thumbnails, | 53 | thumbnails: &mut Thumbnails, |
| 53 | view: &mut View, | ||
| 54 | paper: Paper, | 54 | paper: Paper, |
| 55 | color: Option<u32>, | 55 | color: Option<u32>, |
| 56 | rule_lines: Option<RuleLines>, | 56 | rule_lines: Option<RuleLines>, |
| 57 | colors_only: bool, | ||
| 58 | preview: &mut Option<Option<u32>>, | ||
| 57 | ) -> Option<Choice> { | 59 | ) -> Option<Choice> { |
| 58 | let picker = id.child("custom"); | 60 | let picker = id.child("custom"); |
| 59 | // A pale blue to start from on a page without a colour. | 61 | // A pale blue to start from on a page without a colour. |
| ... | @@ -74,6 +76,9 @@ pub fn menu( | ... | @@ -74,6 +76,9 @@ pub fn menu( |
| 74 | red, green, blue, 0, | 76 | red, green, blue, 0, |
| 75 | ])))); | 77 | ])))); |
| 76 | } | 78 | } |
| 79 | if let Some([red, green, blue]) = ui::popup::picking(ui, picker) { | ||
| 80 | *preview = Some(Some(u32::from_le_bytes([red, green, blue, 0]))); | ||
| 81 | } | ||
| 77 | let custom = color.filter(|color| !PAGE_COLORS.iter().any(|(_, listed)| listed == color)); | 82 | let custom = color.filter(|color| !PAGE_COLORS.iter().any(|(_, listed)| listed == color)); |
| 78 | let cells: Vec<Cell> = std::iter::once(Cell::Color(None)) | 83 | let cells: Vec<Cell> = std::iter::once(Cell::Color(None)) |
| 79 | .chain( | 84 | .chain( |
| ... | @@ -148,7 +153,8 @@ pub fn menu( | ... | @@ -148,7 +153,8 @@ pub fn menu( |
| 148 | }, | 153 | }, |
| 149 | ); | 154 | ); |
| 150 | }; | 155 | }; |
| 151 | let chosen = ui::popup::gallery(ui, id, anchor, &groups, &current, |ui, index| { | 156 | let groups = if colors_only { &groups[..1] } else { &groups }; |
| 157 | let chosen = ui::popup::gallery(ui, id, anchor, groups, &current, |ui, index| { | ||
| 152 | let name = match cells[index] { | 158 | let name = match cells[index] { |
| 153 | Cell::Color(Some(color)) => PAGE_COLORS | 159 | Cell::Color(Some(color)) => PAGE_COLORS |
| 154 | .iter() | 160 | .iter() |
| ... | @@ -236,7 +242,11 @@ pub fn menu( | ... | @@ -236,7 +242,11 @@ pub fn menu( |
| 236 | } | 242 | } |
| 237 | Cell::Rules(Some(index)) => rule_thumbnail(ui, paper, RULE_LINES[index]), | 243 | Cell::Rules(Some(index)) => rule_thumbnail(ui, paper, RULE_LINES[index]), |
| 238 | } | 244 | } |
| 239 | })?; | 245 | }); |
| 246 | if let Some(Cell::Color(color)) = ui::popup::highlight(ui, id).map(|index| cells[index]) { | ||
| 247 | *preview = Some(color); | ||
| 248 | } | ||
| 249 | let chosen = chosen?; | ||
| 240 | match cells[chosen] { | 250 | match cells[chosen] { |
| 241 | Cell::Color(color) => Some(Choice::PageColor(color)), | 251 | Cell::Color(color) => Some(Choice::PageColor(color)), |
| 242 | Cell::Custom => { | 252 | Cell::Custom => { |
| ... | @@ -245,7 +255,7 @@ pub fn menu( | ... | @@ -245,7 +255,7 @@ pub fn menu( |
| 245 | } | 255 | } |
| 246 | Cell::Art(name) => Some(Choice::Art(name)), | 256 | Cell::Art(name) => Some(Choice::Art(name)), |
| 247 | Cell::ShowAll => { | 257 | Cell::ShowAll => { |
| 248 | *view = View::Art; | 258 | ui.open_popup(crate::templates::backgrounds()); |
| 249 | None | 259 | None |
| 250 | } | 260 | } |
| 251 | Cell::Rules(index) => Some(Choice::RuleLines(index)), | 261 | Cell::Rules(index) => Some(Choice::RuleLines(index)), |
crates/snowbound/src/main.rs+19-9| ... | @@ -718,8 +718,8 @@ enum Command { | ... | @@ -718,8 +718,8 @@ enum Command { |
| 718 | space: ExGuid, | 718 | space: ExGuid, |
| 719 | path: String, | 719 | path: String, |
| 720 | }, | 720 | }, |
| 721 | /// Gives the open page a template's background or colour. | 721 | /// Gives the open page a template's background. |
| 722 | Template(templates::Choice), | 722 | Template(&'static str), |
| 723 | OpenPage(ExGuid), | 723 | OpenPage(ExGuid), |
| 724 | /// Shows a page's conflict pages in the list and opens the newest, or hides them. | 724 | /// Shows a page's conflict pages in the list and opens the newest, or hides them. |
| 725 | Versions { | 725 | Versions { |
| ... | @@ -902,8 +902,8 @@ struct State { | ... | @@ -902,8 +902,8 @@ struct State { |
| 902 | dismissed: HashSet<ExGuid>, | 902 | dismissed: HashSet<ExGuid>, |
| 903 | /// A page just created, whose title takes the caret once it opens. | 903 | /// A page just created, whose title takes the caret once it opens. |
| 904 | title_focus: Option<ExGuid>, | 904 | title_focus: Option<ExGuid>, |
| 905 | /// What the template strip shows over a blank page. | 905 | /// The page colour a menu previews over the open page's own this frame. |
| 906 | templates: templates::View, | 906 | page_color_preview: Option<Option<u32>>, |
| 907 | media: recording::Media, | 907 | media: recording::Media, |
| 908 | /// Whether playback highlights the notes linked to the moment playing. | 908 | /// Whether playback highlights the notes linked to the moment playing. |
| 909 | see_playback: bool, | 909 | see_playback: bool, |
| ... | @@ -1349,7 +1349,7 @@ impl State { | ... | @@ -1349,7 +1349,7 @@ impl State { |
| 1349 | .map(|id| notebook::sidecar::themes::successor(&id).to_owned()), | 1349 | .map(|id| notebook::sidecar::themes::successor(&id).to_owned()), |
| 1350 | live_options: stored.live, | 1350 | live_options: stored.live, |
| 1351 | drag: None, | 1351 | drag: None, |
| 1352 | templates: templates::View::Strip, | 1352 | page_color_preview: None, |
| 1353 | media: Default::default(), | 1353 | media: Default::default(), |
| 1354 | see_playback: true, | 1354 | see_playback: true, |
| 1355 | thumbnails: templates::Thumbnails::default(), | 1355 | thumbnails: templates::Thumbnails::default(), |
| ... | @@ -1560,7 +1560,12 @@ impl State { | ... | @@ -1560,7 +1560,12 @@ impl State { |
| 1560 | fn layout(&mut self, size: [f32; 2], scale: f32) -> Result<(), Box<dyn Error>> { | 1560 | fn layout(&mut self, size: [f32; 2], scale: f32) -> Result<(), Box<dyn Error>> { |
| 1561 | self.ui.begin(size, scale, Instant::now()); | 1561 | self.ui.begin(size, scale, Instant::now()); |
| 1562 | platform::cover_border_line(&mut self.ui, size[0]); | 1562 | platform::cover_border_line(&mut self.ui, size[0]); |
| 1563 | let previewed = self.page_color_preview; | ||
| 1563 | let (section, open_tab, open_page) = self.build()?; | 1564 | let (section, open_tab, open_page) = self.build()?; |
| 1565 | // The page's frame and tab were built with last frame's preview. | ||
| 1566 | if self.page_color_preview != previewed { | ||
| 1567 | self.ui.wake_after(std::time::Duration::ZERO); | ||
| 1568 | } | ||
| 1564 | self.options_dialog(); | 1569 | self.options_dialog(); |
| 1565 | self.themes_dialog(); | 1570 | self.themes_dialog(); |
| 1566 | self.print_dialog(); | 1571 | self.print_dialog(); |
| ... | @@ -1677,6 +1682,7 @@ impl State { | ... | @@ -1677,6 +1682,7 @@ impl State { |
| 1677 | // The page, and the open page's tab joined to it, take the page's colour. | 1682 | // The page, and the open page's tab joined to it, take the page's colour. |
| 1678 | let mut theme = self.ui.theme.clone(); | 1683 | let mut theme = self.ui.theme.clone(); |
| 1679 | theme.paper = self.paper().color; | 1684 | theme.paper = self.paper().color; |
| 1685 | self.page_color_preview = None; | ||
| 1680 | platform::update_menu(|| self.statuses()); | 1686 | platform::update_menu(|| self.statuses()); |
| 1681 | let welcome = self.session.is_none() | 1687 | let welcome = self.session.is_none() |
| 1682 | && !self.temporary | 1688 | && !self.temporary |
| ... | @@ -3049,7 +3055,6 @@ impl State { | ... | @@ -3049,7 +3055,6 @@ impl State { |
| 3049 | menu, | 3055 | menu, |
| 3050 | anchor, | 3056 | anchor, |
| 3051 | &mut self.thumbnails, | 3057 | &mut self.thumbnails, |
| 3052 | &mut self.templates, | ||
| 3053 | // The page's own colour aside. | 3058 | // The page's own colour aside. |
| 3054 | canvas::gpu::Paper { | 3059 | canvas::gpu::Paper { |
| 3055 | color: ui.theme.paper, | 3060 | color: ui.theme.paper, |
| ... | @@ -3057,6 +3062,8 @@ impl State { | ... | @@ -3057,6 +3062,8 @@ impl State { |
| 3057 | }, | 3062 | }, |
| 3058 | self.view.editor.page_color(), | 3063 | self.view.editor.page_color(), |
| 3059 | self.view.editor.rule_lines(), | 3064 | self.view.editor.rule_lines(), |
| 3065 | false, | ||
| 3066 | &mut self.page_color_preview, | ||
| 3060 | ) | 3067 | ) |
| 3061 | }, | 3068 | }, |
| 3062 | |_| None, | 3069 | |_| None, |
| ... | @@ -3624,7 +3631,7 @@ impl State { | ... | @@ -3624,7 +3631,7 @@ impl State { |
| 3624 | self.move_page(space, path)?; | 3631 | self.move_page(space, path)?; |
| 3625 | self.edited(Vec::new()); | 3632 | self.edited(Vec::new()); |
| 3626 | } | 3633 | } |
| 3627 | Command::Template(choice) => self.apply_template(choice)?, | 3634 | Command::Template(name) => self.apply_template(name)?, |
| 3628 | Command::Page(Request::EditDate(field)) => self.edit_date(field), | 3635 | Command::Page(Request::EditDate(field)) => self.edit_date(field), |
| 3629 | Command::Page(Request::Copy(clip)) => self.clipboard.set(clip)?, | 3636 | Command::Page(Request::Copy(clip)) => self.clipboard.set(clip)?, |
| 3630 | Command::Page(Request::Paste) => self.paste()?, | 3637 | Command::Page(Request::Paste) => self.paste()?, |
| ... | @@ -4557,13 +4564,16 @@ impl State { | ... | @@ -4557,13 +4564,16 @@ impl State { |
| 4557 | self.window.request_redraw(); | 4564 | self.window.request_redraw(); |
| 4558 | } | 4565 | } |
| 4559 | 4566 | ||
| 4560 | /// The paper the open page lies on: the theme's, in the page's colour. | 4567 | /// The paper the open page lies on: the theme's, in the page's colour or the one previewed. |
| 4561 | fn paper(&self) -> canvas::gpu::Paper { | 4568 | fn paper(&self) -> canvas::gpu::Paper { |
| 4562 | canvas::gpu::Paper { | 4569 | canvas::gpu::Paper { |
| 4563 | color: self.ui.theme.paper, | 4570 | color: self.ui.theme.paper, |
| 4564 | ink: self.ui.theme.paper_ink, | 4571 | ink: self.ui.theme.paper_ink, |
| 4565 | } | 4572 | } |
| 4566 | .colored(self.view.editor.page_color()) | 4573 | .colored( |
| 4574 | self.page_color_preview | ||
| 4575 | .unwrap_or_else(|| self.view.editor.page_color()), | ||
| 4576 | ) | ||
| 4567 | } | 4577 | } |
| 4568 | 4578 | ||
| 4569 | /// The colours of what the page area shows in place of a page, which follow the page's: | 4579 | /// The colours of what the page area shows in place of a page, which follow the page's: |
crates/snowbound/src/manage.rs+32-59| ... | @@ -180,23 +180,13 @@ impl State { | ... | @@ -180,23 +180,13 @@ impl State { |
| 180 | Ok(()) | 180 | Ok(()) |
| 181 | } | 181 | } |
| 182 | 182 | ||
| 183 | /// Gives the open page `choice`'s background: a template's art in place of any it | 183 | /// Gives the open page template `name`'s art in place of any it had. |
| 184 | /// had, or a page colour instead of art. | 184 | pub(crate) fn apply_template(&mut self, name: &'static str) -> Result<(), Box<dyn Error>> { |
| 185 | pub(crate) fn apply_template( | 185 | let template = canvas::template::find(name).ok_or("That template is not available")?; |
| 186 | &mut self, | 186 | self.with_art(Some(template), move |state, art| { |
| 187 | choice: crate::templates::Choice, | ||
| 188 | ) -> Result<(), Box<dyn Error>> { | ||
| 189 | use crate::templates::Choice; | ||
| 190 | let template = match choice { | ||
| 191 | Choice::Template(name) => { | ||
| 192 | Some(canvas::template::find(name).ok_or("That template is not available")?) | ||
| 193 | } | ||
| 194 | _ => None, | ||
| 195 | }; | ||
| 196 | self.with_art(template, move |state, art| { | ||
| 197 | state.persist()?; | 187 | state.persist()?; |
| 198 | let session = state.session.as_ref().ok_or("No section is open")?; | 188 | let session = state.session.as_ref().ok_or("No section is open")?; |
| 199 | let ops = template_ops(&session.section.page(session.space)?, choice, art)?; | 189 | let ops = template_ops(&session.section.page(session.space)?, name, art)?; |
| 200 | state.edit_page(ops) | 190 | state.edit_page(ops) |
| 201 | }) | 191 | }) |
| 202 | } | 192 | } |
| ... | @@ -561,7 +551,6 @@ impl State { | ... | @@ -561,7 +551,6 @@ impl State { |
| 561 | self.persist().unwrap_or_else(|error| eprintln!("{error}")); | 551 | self.persist().unwrap_or_else(|error| eprintln!("{error}")); |
| 562 | self.session = None; | 552 | self.session = None; |
| 563 | self.sectionless = None; | 553 | self.sectionless = None; |
| 564 | self.templates = crate::templates::View::Strip; | ||
| 565 | match self | 554 | match self |
| 566 | .notebooks | 555 | .notebooks |
| 567 | .iter() | 556 | .iter() |
| ... | @@ -966,35 +955,23 @@ impl State { | ... | @@ -966,35 +955,23 @@ impl State { |
| 966 | } | 955 | } |
| 967 | } | 956 | } |
| 968 | 957 | ||
| 969 | /// The ops giving `page` `choice`'s background: its template art or a page colour, in | 958 | /// The ops giving `page` template `name`'s art, `art`, in place of the background it had, |
| 970 | /// place of the background it had. | 959 | /// and no page colour. |
| 971 | pub fn template_ops( | 960 | pub fn template_ops( |
| 972 | page: &Page, | 961 | page: &Page, |
| 973 | choice: crate::templates::Choice, | 962 | name: &str, |
| 974 | art: Vec<Image>, | 963 | art: Vec<Image>, |
| 975 | ) -> Result<Vec<PageOp>, Box<dyn Error>> { | 964 | ) -> Result<Vec<PageOp>, Box<dyn Error>> { |
| 976 | use crate::templates::Choice; | 965 | let mut ops = art_ops(page, art); |
| 977 | let mut ops = match choice { | 966 | if page.color.is_some() { |
| 978 | Choice::Template(_) | Choice::Color(_) => art_ops(page, art), | 967 | ops.push(PageOp::Color(None)); |
| 979 | Choice::More | Choice::Dismiss | Choice::Colors => return Ok(Vec::new()), | 968 | } |
| 980 | }; | 969 | // The one template with content worth keeping. |
| 981 | match choice { | 970 | if name == "Informal Meeting Notes" { |
| 982 | Choice::Template(name) => { | 971 | ops.extend(onestore::op::lower_page( |
| 983 | if page.color.is_some() { | 972 | page, |
| 984 | ops.push(PageOp::Color(None)); | 973 | &crate::meeting::content(page)?, |
| 985 | } | 974 | )?); |
| 986 | // The one template with content worth keeping. | ||
| 987 | if name == "Informal Meeting Notes" { | ||
| 988 | ops.extend(onestore::op::lower_page( | ||
| 989 | page, | ||
| 990 | &crate::meeting::content(page)?, | ||
| 991 | )?); | ||
| 992 | } | ||
| 993 | } | ||
| 994 | Choice::Color(index) => { | ||
| 995 | ops.push(PageOp::Color(Some(canvas::template::PAGE_COLORS[index].1))) | ||
| 996 | } | ||
| 997 | Choice::More | Choice::Dismiss | Choice::Colors => {} | ||
| 998 | } | 975 | } |
| 999 | Ok(ops) | 976 | Ok(ops) |
| 1000 | } | 977 | } |
| ... | @@ -1031,7 +1008,6 @@ fn art_ops(page: &Page, art: Vec<Image>) -> Vec<PageOp> { | ... | @@ -1031,7 +1008,6 @@ fn art_ops(page: &Page, art: Vec<Image>) -> Vec<PageOp> { |
| 1031 | #[cfg(test)] | 1008 | #[cfg(test)] |
| 1032 | mod tests { | 1009 | mod tests { |
| 1033 | use super::*; | 1010 | use super::*; |
| 1034 | use crate::templates::Choice; | ||
| 1035 | use std::path::Path; | 1011 | use std::path::Path; |
| 1036 | 1012 | ||
| 1037 | const AUTHOR: &str = "Rust Author"; | 1013 | const AUTHOR: &str = "Rust Author"; |
| ... | @@ -1153,27 +1129,27 @@ mod tests { | ... | @@ -1153,27 +1129,27 @@ mod tests { |
| 1153 | let [ivy, teal, subpage, deleted, meeting] = spaces[..] else { | 1129 | let [ivy, teal, subpage, deleted, meeting] = spaces[..] else { |
| 1154 | unreachable!() | 1130 | unreachable!() |
| 1155 | }; | 1131 | }; |
| 1156 | let background = |space: ExGuid, choice: Choice| { | 1132 | let background = |space: ExGuid, name: &str| { |
| 1157 | let art = match choice { | 1133 | let art = canvas::template::find(name).unwrap().pictures().unwrap(); |
| 1158 | Choice::Template(name) => canvas::template::find(name).unwrap().pictures().unwrap(), | 1134 | template_ops(&page(&file, space), name, art) |
| 1159 | _ => Vec::new(), | ||
| 1160 | }; | ||
| 1161 | template_ops(&page(&file, space), choice, art) | ||
| 1162 | .unwrap() | 1135 | .unwrap() |
| 1163 | .into_iter() | 1136 | .into_iter() |
| 1164 | .map(|op| Op::Page { space, op }) | 1137 | .map(|op| Op::Page { space, op }) |
| 1165 | .collect::<Vec<_>>() | 1138 | .collect::<Vec<_>>() |
| 1166 | }; | 1139 | }; |
| 1167 | edit(&file, background(ivy, Choice::Template("Ivy"))); | 1140 | edit(&file, background(ivy, "Ivy")); |
| 1141 | edit(&file, background(meeting, "Informal Meeting Notes")); | ||
| 1142 | let (_, teal_color) = canvas::template::PAGE_COLORS | ||
| 1143 | .iter() | ||
| 1144 | .find(|(name, _)| *name == "Teal") | ||
| 1145 | .unwrap(); | ||
| 1168 | edit( | 1146 | edit( |
| 1169 | &file, | 1147 | &file, |
| 1170 | background(meeting, Choice::Template("Informal Meeting Notes")), | 1148 | vec![Op::Page { |
| 1149 | space: teal, | ||
| 1150 | op: PageOp::Color(Some(*teal_color)), | ||
| 1151 | }], | ||
| 1171 | ); | 1152 | ); |
| 1172 | let teal_index = canvas::template::PAGE_COLORS | ||
| 1173 | .iter() | ||
| 1174 | .position(|(name, _)| *name == "Teal") | ||
| 1175 | .unwrap(); | ||
| 1176 | edit(&file, background(teal, Choice::Color(teal_index))); | ||
| 1177 | edit( | 1153 | edit( |
| 1178 | &file, | 1154 | &file, |
| 1179 | vec![Op::Section(SectionOp::Pages(vec![ | 1155 | vec![Op::Section(SectionOp::Pages(vec![ |
| ... | @@ -1275,10 +1251,7 @@ mod tests { | ... | @@ -1275,10 +1251,7 @@ mod tests { |
| 1275 | ] | 1251 | ] |
| 1276 | ); | 1252 | ); |
| 1277 | let shown = page(&file, teal); | 1253 | let shown = page(&file, teal); |
| 1278 | assert_eq!( | 1254 | assert_eq!(shown.color, Some(*teal_color)); |
| 1279 | shown.color, | ||
| 1280 | Some(canvas::template::PAGE_COLORS[teal_index].1) | ||
| 1281 | ); | ||
| 1282 | assert!(shown.date_text().is_some()); | 1255 | assert!(shown.date_text().is_some()); |
| 1283 | assert!( | 1256 | assert!( |
| 1284 | page(&file, ivy) | 1257 | page(&file, ivy) |
crates/snowbound/src/print.rs+9-6| ... | @@ -530,12 +530,15 @@ mod tests { | ... | @@ -530,12 +530,15 @@ mod tests { |
| 530 | }; | 530 | }; |
| 531 | let create = |creation| Op::Section(SectionOp::Create(creation)); | 531 | let create = |creation| Op::Section(SectionOp::Create(creation)); |
| 532 | (section.apply("Author", edit(vec![create(dated), create(written)]))).unwrap(); | 532 | (section.apply("Author", edit(vec![create(dated), create(written)]))).unwrap(); |
| 533 | let meeting = crate::templates::Choice::Template("Informal Meeting Notes"); | 533 | let ops = crate::manage::template_ops( |
| 534 | let ops = crate::manage::template_ops(&section.page(space).unwrap(), meeting, Vec::new()) | 534 | &section.page(space).unwrap(), |
| 535 | .unwrap() | 535 | "Informal Meeting Notes", |
| 536 | .into_iter() | 536 | Vec::new(), |
| 537 | .map(|op| Op::Page { space, op }) | 537 | ) |
| 538 | .collect(); | 538 | .unwrap() |
| 539 | .into_iter() | ||
| 540 | .map(|op| Op::Page { space, op }) | ||
| 541 | .collect(); | ||
| 539 | section.apply("Author", edit(ops)).unwrap(); | 542 | section.apply("Author", edit(ops)).unwrap(); |
| 540 | // The template titles the page; its title goes, its text stays. | 543 | // The template titles the page; its title goes, its text stays. |
| 541 | let untitled = crate::rename::retitled(&section.page(space).unwrap(), space, String::new()); | 544 | let untitled = crate::rename::retitled(&section.page(space).unwrap(), space, String::new()); |
crates/snowbound/src/templates.rs+143-202| ... | @@ -1,19 +1,18 @@ | ... | @@ -1,19 +1,18 @@ |
| 1 | //! The template strip a new page shows where its body would start until the body is typed | 1 | //! The template strip a new page shows where its body would start until the body is typed |
| 2 | //! in, and the gallery of every template it leads to. | 2 | //! in, and the galleries of every template docked beside the page. |
| 3 | 3 | ||
| 4 | use crate::art; | 4 | use crate::art; |
| 5 | use canvas::{ | 5 | use canvas::{ |
| 6 | gpu::Paper, | 6 | gpu::Paper, |
| 7 | template::{PAGE_COLORS, TEMPLATES, Template}, | 7 | template::{TEMPLATES, Template}, |
| 8 | }; | 8 | }; |
| 9 | use draw::RasterImage; | 9 | use draw::RasterImage; |
| 10 | use std::collections::HashMap; | 10 | use std::collections::HashMap; |
| 11 | use ui::{Axis, Flags, Spec, Theme, Ui, fill, px}; | 11 | use ui::{Anchor, Axis, Flags, Id, Spec, Theme, Ui, fill, px}; |
| 12 | 12 | ||
| 13 | /// What a thumbnail shows: the top of a letter page, in points from the page origin. | 13 | /// What a thumbnail shows: the top of a letter page, in points from the page origin. |
| 14 | const REGION: [f32; 4] = [-72.0, -40.0, 540.0, 419.0]; | 14 | const REGION: [f32; 4] = [-72.0, -40.0, 540.0, 419.0]; |
| 15 | pub(crate) const TILE: [f32; 2] = [112.0, 84.0]; | 15 | pub(crate) const TILE: [f32; 2] = [112.0, 84.0]; |
| 16 | const SWATCH: f32 = 24.0; | ||
| 17 | const LABEL: f32 = 22.0; | 16 | const LABEL: f32 = 22.0; |
| 18 | const GAP: f32 = 12.0; | 17 | const GAP: f32 = 12.0; |
| 19 | const PAD: f32 = 10.0; | 18 | const PAD: f32 = 10.0; |
| ... | @@ -30,26 +29,28 @@ const STRIP: [(Choice, &str); 5] = [ | ... | @@ -30,26 +29,28 @@ const STRIP: [(Choice, &str); 5] = [ |
| 30 | ]; | 29 | ]; |
| 31 | 30 | ||
| 32 | #[derive(Clone, Copy, Debug, PartialEq)] | 31 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 33 | pub enum Choice { | 32 | enum Choice { |
| 34 | Template(&'static str), | 33 | Template(&'static str), |
| 35 | /// An index into `PAGE_COLORS`. | 34 | /// Opens the Page Color menu over its tile. |
| 36 | Color(usize), | ||
| 37 | /// Shows or hides the page colours. | ||
| 38 | Colors, | 35 | Colors, |
| 39 | More, | 36 | More, |
| 40 | Dismiss, | 37 | Dismiss, |
| 41 | } | 38 | } |
| 42 | 39 | ||
| 43 | /// What the template strip shows. | 40 | /// The gallery of every template's art, from the Page Color menu's Show All, to lie behind |
| 44 | #[derive(Clone, Copy, Debug, Default, PartialEq)] | 41 | /// the open page. |
| 45 | pub enum View { | 42 | pub fn backgrounds() -> Id { |
| 46 | #[default] | 43 | Id::ROOT.child("backgrounds") |
| 47 | Strip, | 44 | } |
| 48 | /// The strip with the page colours beneath it. | 45 | |
| 49 | Colors, | 46 | /// The gallery of every template, from the strip's More templates. |
| 50 | Gallery, | 47 | fn templates() -> Id { |
| 51 | /// Every template's art, from the Page Color menu, to lie behind the open page. | 48 | Id::ROOT.child("templates") |
| 52 | Art, | 49 | } |
| 50 | |||
| 51 | /// The page colours the strip's Solid Color opens. | ||
| 52 | fn colors() -> Id { | ||
| 53 | Id::ROOT.child("strip colors") | ||
| 53 | } | 54 | } |
| 54 | 55 | ||
| 55 | /// A thumbnail's art: each raster and where it lies in the tile. | 56 | /// A thumbnail's art: each raster and where it lies in the tile. |
| ... | @@ -87,20 +88,17 @@ impl Thumbnails { | ... | @@ -87,20 +88,17 @@ impl Thumbnails { |
| 87 | } | 88 | } |
| 88 | } | 89 | } |
| 89 | 90 | ||
| 90 | /// The strip at `position` in the page box, `room` wide at most, with the page colours | 91 | /// The strip at `position` in the page box, `room` wide at most. Picks that do not fit give |
| 91 | /// beneath while `colors`. Picks that do not fit give way from the end; More templates | 92 | /// way from the end; More templates stays. Returns the pick and the Solid Color tile, where |
| 92 | /// stays. | 93 | /// shown. |
| 93 | #[allow(clippy::too_many_arguments)] | ||
| 94 | fn strip( | 94 | fn strip( |
| 95 | ui: &mut Ui, | 95 | ui: &mut Ui, |
| 96 | theme: &Theme, | 96 | theme: &Theme, |
| 97 | thumbnails: &mut Thumbnails, | 97 | thumbnails: &mut Thumbnails, |
| 98 | paper: Paper, | 98 | paper: Paper, |
| 99 | dark: bool, | ||
| 100 | position: [f32; 2], | 99 | position: [f32; 2], |
| 101 | room: f32, | 100 | room: f32, |
| 102 | colors: bool, | 101 | ) -> (Option<Choice>, Option<Id>) { |
| 103 | ) -> Option<Choice> { | ||
| 104 | let more = (Choice::More, "More templates"); | 102 | let more = (Choice::More, "More templates"); |
| 105 | let mut used = 2.0 * PAD + tile_width(ui, more.1); | 103 | let mut used = 2.0 * PAD + tile_width(ui, more.1); |
| 106 | let mut shown = Vec::new(); | 104 | let mut shown = Vec::new(); |
| ... | @@ -117,6 +115,7 @@ fn strip( | ... | @@ -117,6 +115,7 @@ fn strip( |
| 117 | .sum::<f32>() | 115 | .sum::<f32>() |
| 118 | - GAP; | 116 | - GAP; |
| 119 | let mut chosen = None; | 117 | let mut chosen = None; |
| 118 | let mut solid = None; | ||
| 120 | ui.open( | 119 | ui.open( |
| 121 | "templates", | 120 | "templates", |
| 122 | Spec { | 121 | Spec { |
| ... | @@ -128,7 +127,6 @@ fn strip( | ... | @@ -128,7 +127,6 @@ fn strip( |
| 128 | shadow: Some(theme.shadow), | 127 | shadow: Some(theme.shadow), |
| 129 | radius: 8.0, | 128 | radius: 8.0, |
| 130 | pad: [PAD, PAD], | 129 | pad: [PAD, PAD], |
| 131 | gap: 8.0, | ||
| 132 | ..Spec::default() | 130 | ..Spec::default() |
| 133 | }, | 131 | }, |
| 134 | ); | 132 | ); |
| ... | @@ -141,46 +139,16 @@ fn strip( | ... | @@ -141,46 +139,16 @@ fn strip( |
| 141 | }, | 139 | }, |
| 142 | ); | 140 | ); |
| 143 | for (index, (choice, label)) in shown.into_iter().enumerate() { | 141 | for (index, (choice, label)) in shown.into_iter().enumerate() { |
| 144 | if tile(ui, theme, thumbnails, paper, dark, index, choice, label) { | 142 | let width = tile_width(ui, label); |
| 143 | let tile = tile(ui, theme, thumbnails, paper, index, choice, label, width); | ||
| 144 | if choice == Choice::Colors { | ||
| 145 | solid = Some(tile); | ||
| 146 | } | ||
| 147 | if ui.signal(tile).clicked { | ||
| 145 | chosen = Some(choice); | 148 | chosen = Some(choice); |
| 146 | } | 149 | } |
| 147 | } | 150 | } |
| 148 | ui.close(); | 151 | ui.close(); |
| 149 | let per_row = (((room - 2.0 * PAD + 6.0) / (SWATCH + 6.0)).floor() as usize).max(1); | ||
| 150 | for (row, swatches) in PAGE_COLORS.chunks(per_row).enumerate().filter(|_| colors) { | ||
| 151 | ui.open( | ||
| 152 | ("colors", row), | ||
| 153 | Spec { | ||
| 154 | size: [ui::children(), px(SWATCH)], | ||
| 155 | gap: 6.0, | ||
| 156 | ..Spec::default() | ||
| 157 | }, | ||
| 158 | ); | ||
| 159 | for (index, (name, color)) in swatches | ||
| 160 | .iter() | ||
| 161 | .enumerate() | ||
| 162 | .map(|(index, color)| (row * per_row + index, color)) | ||
| 163 | { | ||
| 164 | let swatch = ui.leaf( | ||
| 165 | (index, *name), | ||
| 166 | Spec { | ||
| 167 | flags: Flags::CLICKABLE, | ||
| 168 | size: [px(SWATCH), px(SWATCH)], | ||
| 169 | fill: Some(page_color(*color, paper, dark)), | ||
| 170 | border: Some(theme.chip), | ||
| 171 | hover_border: Some(theme.accent), | ||
| 172 | radius: 4.0, | ||
| 173 | role: Some(accesskit::Role::Button), | ||
| 174 | ..Spec::default() | ||
| 175 | }, | ||
| 176 | ); | ||
| 177 | crate::name(ui, ui.id((index, *name)), name); | ||
| 178 | if swatch.clicked { | ||
| 179 | chosen = Some(Choice::Color(index)); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | ui.close(); | ||
| 183 | } | ||
| 184 | let close = ui.leaf( | 152 | let close = ui.leaf( |
| 185 | "dismiss", | 153 | "dismiss", |
| 186 | Spec { | 154 | Spec { |
| ... | @@ -203,37 +171,45 @@ fn strip( | ... | @@ -203,37 +171,45 @@ fn strip( |
| 203 | chosen = Some(Choice::Dismiss); | 171 | chosen = Some(Choice::Dismiss); |
| 204 | } | 172 | } |
| 205 | ui.close(); | 173 | ui.close(); |
| 206 | chosen | 174 | (chosen, solid) |
| 207 | } | 175 | } |
| 208 | 176 | ||
| 209 | /// Every template, or with `art` only their art, as a scrolling grid over the page box | 177 | /// Dialog `id`, titled `title`, of every template docked inside the page box's trailing edge, |
| 210 | /// `size` big. | 178 | /// `height` tall, while it is open. Returns the template chosen; it stays open to choose again. |
| 179 | #[allow(clippy::too_many_arguments)] | ||
| 211 | fn gallery( | 180 | fn gallery( |
| 212 | ui: &mut Ui, | 181 | ui: &mut Ui, |
| 213 | theme: &Theme, | 182 | theme: &Theme, |
| 214 | thumbnails: &mut Thumbnails, | 183 | thumbnails: &mut Thumbnails, |
| 215 | paper: Paper, | 184 | paper: Paper, |
| 216 | dark: bool, | 185 | id: Id, |
| 217 | size: [f32; 2], | 186 | title: &str, |
| 218 | art: bool, | 187 | height: f32, |
| 219 | ) -> Option<Choice> { | 188 | ) -> Option<&'static str> { |
| 189 | if !ui.popup_open(id) { | ||
| 190 | return None; | ||
| 191 | } | ||
| 220 | let mut chosen = None; | 192 | let mut chosen = None; |
| 221 | let margin = 24.0; | 193 | let column = TILE[0] + 40.0; |
| 222 | ui.open( | 194 | ui.open_as( |
| 223 | "gallery", | 195 | id, |
| 224 | Spec { | 196 | Spec { |
| 225 | flags: Flags::FLOAT | Flags::CLICKABLE, | ||
| 226 | axis: Axis::Y, | 197 | axis: Axis::Y, |
| 227 | size: [px(size[0] - 2.0 * margin), px(size[1] - 2.0 * margin)], | 198 | size: [px(2.0 * column + GAP + 2.0 * PAD), px(height)], |
| 228 | position: [margin, margin], | ||
| 229 | fill: Some(theme.popup), | 199 | fill: Some(theme.popup), |
| 200 | border: Some(theme.chip), | ||
| 230 | shadow: Some(theme.shadow), | 201 | shadow: Some(theme.shadow), |
| 231 | radius: 8.0, | 202 | radius: 8.0, |
| 232 | pad: [PAD, PAD], | 203 | pad: [PAD, PAD], |
| 233 | gap: 8.0, | 204 | gap: 8.0, |
| 205 | anchor: Some(Anchor::Dock(crate::page())), | ||
| 206 | role: Some(accesskit::Role::Dialog), | ||
| 234 | ..Spec::default() | 207 | ..Spec::default() |
| 235 | }, | 208 | }, |
| 236 | ); | 209 | ); |
| 210 | if let Some(node) = ui.access(id) { | ||
| 211 | node.set_label(title); | ||
| 212 | } | ||
| 237 | ui.open( | 213 | ui.open( |
| 238 | "header", | 214 | "header", |
| 239 | Spec { | 215 | Spec { |
| ... | @@ -245,12 +221,14 @@ fn gallery( | ... | @@ -245,12 +221,14 @@ fn gallery( |
| 245 | "title", | 221 | "title", |
| 246 | Spec { | 222 | Spec { |
| 247 | size: [fill(), px(24.0)], | 223 | size: [fill(), px(24.0)], |
| 248 | text: Some(if art { "Backgrounds" } else { "Page Templates" }), | 224 | text: Some(title), |
| 225 | bold: true, | ||
| 226 | role: Some(accesskit::Role::Heading), | ||
| 249 | ..Spec::default() | 227 | ..Spec::default() |
| 250 | }, | 228 | }, |
| 251 | ); | 229 | ); |
| 252 | if ui::shell::tool_button(ui, "close", art::CLOSE, theme.text_dim, None).clicked { | 230 | if ui::shell::tool_button(ui, "close", art::CLOSE, theme.text_dim, None).clicked { |
| 253 | chosen = Some(Choice::Dismiss); | 231 | ui.close_popup(id); |
| 254 | } | 232 | } |
| 255 | crate::name(ui, ui.id("close"), "Close"); | 233 | crate::name(ui, ui.id("close"), "Close"); |
| 256 | ui.close(); | 234 | ui.close(); |
| ... | @@ -264,21 +242,7 @@ fn gallery( | ... | @@ -264,21 +242,7 @@ fn gallery( |
| 264 | ..Spec::default() | 242 | ..Spec::default() |
| 265 | }, | 243 | }, |
| 266 | ); | 244 | ); |
| 267 | let column = TILE[0] + 40.0; | 245 | for (row, templates) in TEMPLATES.chunks(2).enumerate() { |
| 268 | let columns = | ||
| 269 | (((size[0] - 2.0 * margin - 2.0 * PAD + GAP) / (column + GAP)).floor() as usize).max(1); | ||
| 270 | let choices: Vec<(Choice, &str)> = TEMPLATES | ||
| 271 | .iter() | ||
| 272 | .map(|template| (Choice::Template(template.name), template.name)) | ||
| 273 | .chain( | ||
| 274 | PAGE_COLORS | ||
| 275 | .iter() | ||
| 276 | .enumerate() | ||
| 277 | .map(|(index, (name, _))| (Choice::Color(index), *name)) | ||
| 278 | .filter(|_| !art), | ||
| 279 | ) | ||
| 280 | .collect(); | ||
| 281 | for (row, choices) in choices.chunks(columns).enumerate() { | ||
| 282 | ui.open( | 246 | ui.open( |
| 283 | row, | 247 | row, |
| 284 | Spec { | 248 | Spec { |
| ... | @@ -287,9 +251,20 @@ fn gallery( | ... | @@ -287,9 +251,20 @@ fn gallery( |
| 287 | ..Spec::default() | 251 | ..Spec::default() |
| 288 | }, | 252 | }, |
| 289 | ); | 253 | ); |
| 290 | for (index, (choice, label)) in choices.iter().enumerate() { | 254 | for (index, template) in templates.iter().enumerate() { |
| 291 | if tile(ui, theme, thumbnails, paper, dark, index, *choice, label) { | 255 | let choice = Choice::Template(template.name); |
| 292 | chosen = Some(*choice); | 256 | let tile = tile( |
| 257 | ui, | ||
| 258 | theme, | ||
| 259 | thumbnails, | ||
| 260 | paper, | ||
| 261 | index, | ||
| 262 | choice, | ||
| 263 | template.name, | ||
| 264 | column, | ||
| 265 | ); | ||
| 266 | if ui.signal(tile).clicked { | ||
| 267 | chosen = Some(template.name); | ||
| 293 | } | 268 | } |
| 294 | } | 269 | } |
| 295 | ui.close(); | 270 | ui.close(); |
| ... | @@ -299,43 +274,26 @@ fn gallery( | ... | @@ -299,43 +274,26 @@ fn gallery( |
| 299 | chosen | 274 | chosen |
| 300 | } | 275 | } |
| 301 | 276 | ||
| 302 | /// A page colour as the paper shows it: on dark paper, its hue at the paper's lightness. | ||
| 303 | fn page_color(color: u32, paper: Paper, dark: bool) -> [f32; 4] { | ||
| 304 | let color = canvas::gpu::colorref(color); | ||
| 305 | if !dark { | ||
| 306 | return color; | ||
| 307 | } | ||
| 308 | let [lightness, ..] = draw::oklab(paper.color); | ||
| 309 | let [_, a, b] = draw::oklab(color); | ||
| 310 | let [red, green, blue] = draw::from_oklab([lightness + 0.04, 2.0 * a, 2.0 * b]); | ||
| 311 | [red, green, blue, 1.0] | ||
| 312 | } | ||
| 313 | |||
| 314 | /// How wide a tile labelled `label` stands: its thumbnail, or the label where it is wider. | 277 | /// How wide a tile labelled `label` stands: its thumbnail, or the label where it is wider. |
| 315 | fn tile_width(ui: &mut Ui, label: &str) -> f32 { | 278 | fn tile_width(ui: &mut Ui, label: &str) -> f32 { |
| 316 | TILE[0].max(ui.measure(label)[0] + 4.0) | 279 | TILE[0].max(ui.measure(label)[0] + 4.0) |
| 317 | } | 280 | } |
| 318 | 281 | ||
| 319 | /// One template's thumbnail over its label, as wide as the thumbnail or the label; true | 282 | /// One template's thumbnail over its label, `width` wide; returns the thumbnail, which takes |
| 320 | /// when clicked. | 283 | /// the click. |
| 321 | #[allow(clippy::too_many_arguments)] | 284 | #[allow(clippy::too_many_arguments)] |
| 322 | fn tile( | 285 | fn tile( |
| 323 | ui: &mut Ui, | 286 | ui: &mut Ui, |
| 324 | theme: &Theme, | 287 | theme: &Theme, |
| 325 | thumbnails: &mut Thumbnails, | 288 | thumbnails: &mut Thumbnails, |
| 326 | paper: Paper, | 289 | paper: Paper, |
| 327 | dark: bool, | ||
| 328 | part: usize, | 290 | part: usize, |
| 329 | choice: Choice, | 291 | choice: Choice, |
| 330 | label: &str, | 292 | label: &str, |
| 331 | ) -> bool { | 293 | width: f32, |
| 294 | ) -> Id { | ||
| 332 | let scale = ui.scale(); | 295 | let scale = ui.scale(); |
| 333 | let width = tile_width(ui, label); | 296 | let dark = paper.ink[0] > paper.color[0]; |
| 334 | let fill_color = match choice { | ||
| 335 | Choice::Color(index) => page_color(PAGE_COLORS[index].1, paper, dark), | ||
| 336 | Choice::More | Choice::Dismiss => theme.base, | ||
| 337 | Choice::Template(_) | Choice::Colors => paper.color, | ||
| 338 | }; | ||
| 339 | ui.open( | 297 | ui.open( |
| 340 | (part, label), | 298 | (part, label), |
| 341 | Spec { | 299 | Spec { |
| ... | @@ -350,7 +308,11 @@ fn tile( | ... | @@ -350,7 +308,11 @@ fn tile( |
| 350 | Spec { | 308 | Spec { |
| 351 | flags: Flags::CLICKABLE | Flags::CLIP, | 309 | flags: Flags::CLICKABLE | Flags::CLIP, |
| 352 | size: [px(TILE[0]), px(TILE[1])], | 310 | size: [px(TILE[0]), px(TILE[1])], |
| 353 | fill: Some(fill_color), | 311 | fill: Some(if choice == Choice::More { |
| 312 | theme.base | ||
| 313 | } else { | ||
| 314 | paper.color | ||
| 315 | }), | ||
| 354 | border: Some(theme.chip), | 316 | border: Some(theme.chip), |
| 355 | hover_border: Some(theme.accent), | 317 | hover_border: Some(theme.accent), |
| 356 | radius: 4.0, | 318 | radius: 4.0, |
| ... | @@ -383,10 +345,11 @@ fn tile( | ... | @@ -383,10 +345,11 @@ fn tile( |
| 383 | } | 345 | } |
| 384 | } | 346 | } |
| 385 | } | 347 | } |
| 386 | // A band of each of the first page colours. | 348 | // A band of each of the first page colours, as the page shows them. |
| 387 | Choice::Colors => { | 349 | Choice::Colors => { |
| 388 | let bands = 4; | 350 | let bands = 4; |
| 389 | for (index, (_, color)) in PAGE_COLORS.iter().take(bands).enumerate() { | 351 | for (index, (_, color)) in canvas::template::PAGE_COLORS.iter().take(bands).enumerate() |
| 352 | { | ||
| 390 | let band = TILE[0] / bands as f32; | 353 | let band = TILE[0] / bands as f32; |
| 391 | ui.leaf( | 354 | ui.leaf( |
| 392 | ("band", index), | 355 | ("band", index), |
| ... | @@ -394,13 +357,13 @@ fn tile( | ... | @@ -394,13 +357,13 @@ fn tile( |
| 394 | flags: Flags::FLOAT, | 357 | flags: Flags::FLOAT, |
| 395 | size: [px(band), px(TILE[1])], | 358 | size: [px(band), px(TILE[1])], |
| 396 | position: [band * index as f32, 0.0], | 359 | position: [band * index as f32, 0.0], |
| 397 | fill: Some(page_color(*color, paper, dark)), | 360 | fill: Some(paper.colored(Some(*color)).color), |
| 398 | ..Spec::default() | 361 | ..Spec::default() |
| 399 | }, | 362 | }, |
| 400 | ); | 363 | ); |
| 401 | } | 364 | } |
| 402 | } | 365 | } |
| 403 | _ => {} | 366 | Choice::More | Choice::Dismiss => {} |
| 404 | } | 367 | } |
| 405 | ui.close(); | 368 | ui.close(); |
| 406 | ui.leaf( | 369 | ui.leaf( |
| ... | @@ -414,121 +377,99 @@ fn tile( | ... | @@ -414,121 +377,99 @@ fn tile( |
| 414 | }, | 377 | }, |
| 415 | ); | 378 | ); |
| 416 | ui.close(); | 379 | ui.close(); |
| 417 | ui.signal(id).clicked | 380 | id |
| 418 | } | 381 | } |
| 419 | 382 | ||
| 420 | impl crate::State { | 383 | impl crate::State { |
| 421 | /// Over a page with nothing typed in its body: the strip where the body would start, | 384 | /// The template galleries while open, and over a page with nothing typed in its body the |
| 422 | /// or the gallery it opened. | 385 | /// strip where the body would start. |
| 423 | pub(crate) fn template_strip(&mut self, theme: &Theme) { | 386 | pub(crate) fn template_strip(&mut self, theme: &Theme) { |
| 424 | let Some(session) = &self.session else { | 387 | let Some(session) = &self.session else { |
| 425 | return; | 388 | return; |
| 426 | }; | 389 | }; |
| 427 | if self.templates == View::Art { | ||
| 428 | return self.art_gallery(theme); | ||
| 429 | } | ||
| 430 | let space = session.space; | 390 | let space = session.space; |
| 391 | let read_only = session.read_only(); | ||
| 392 | let Some(rect) = self.ui.laid_out(crate::page()) else { | ||
| 393 | return; | ||
| 394 | }; | ||
| 395 | // Thumbnails show templates on plain paper, not the page's colour. | ||
| 396 | let paper = Paper { | ||
| 397 | color: self.ui.theme.paper, | ||
| 398 | ink: self.ui.theme.paper_ink, | ||
| 399 | }; | ||
| 400 | let height = rect[3] - rect[1] - 8.0; | ||
| 401 | if let Some(name) = gallery( | ||
| 402 | &mut self.ui, | ||
| 403 | theme, | ||
| 404 | &mut self.thumbnails, | ||
| 405 | paper, | ||
| 406 | backgrounds(), | ||
| 407 | "Backgrounds", | ||
| 408 | height, | ||
| 409 | ) { | ||
| 410 | self.choose(crate::commands::Choice::Art(Some(name))); | ||
| 411 | } | ||
| 431 | let editor = &self.view.editor; | 412 | let editor = &self.view.editor; |
| 432 | let blank = editor | 413 | let blank = editor |
| 433 | .visible_outlines() | 414 | .visible_outlines() |
| 434 | .chain(editor.caret_outline()) | 415 | .chain(editor.caret_outline()) |
| 435 | .filter(|outline| !outline.title) | 416 | .filter(|outline| !outline.title) |
| 436 | .all(canvas::editor::TextOutline::is_empty); | 417 | .all(canvas::editor::TextOutline::is_empty); |
| 437 | if !blank || session.read_only() || self.dismissed.contains(&space) { | 418 | if !blank || read_only || self.dismissed.contains(&space) { |
| 438 | self.templates = View::Strip; | ||
| 439 | return; | 419 | return; |
| 440 | } | 420 | } |
| 441 | let (Some(rect), Some(start)) = (self.ui.laid_out(crate::page()), editor.body_start()) | 421 | let Some(start) = editor.body_start() else { |
| 442 | else { | ||
| 443 | return; | 422 | return; |
| 444 | }; | 423 | }; |
| 424 | let (color, rule_lines) = (editor.page_color(), editor.rule_lines()); | ||
| 445 | let viewport = self.view.viewport; | 425 | let viewport = self.view.viewport; |
| 446 | let scale = self.ui.scale(); | 426 | let scale = self.ui.scale(); |
| 447 | let position = [ | 427 | let position = [ |
| 448 | (viewport.origin[0] + start[0] * viewport.scale) / scale, | 428 | (viewport.origin[0] + start[0] * viewport.scale) / scale, |
| 449 | (viewport.origin[1] + (start[1] + LINES_ABOVE) * viewport.scale) / scale, | 429 | (viewport.origin[1] + (start[1] + LINES_ABOVE) * viewport.scale) / scale, |
| 450 | ]; | 430 | ]; |
| 451 | // Thumbnails show templates on plain paper, not the page's colour. | 431 | let (chosen, solid) = strip( |
| 452 | let paper = Paper { | 432 | &mut self.ui, |
| 453 | color: self.ui.theme.paper, | 433 | theme, |
| 454 | ink: self.ui.theme.paper_ink, | 434 | &mut self.thumbnails, |
| 455 | }; | 435 | paper, |
| 456 | let dark = paper.ink[0] > paper.color[0]; | 436 | position, |
| 457 | let chosen = match self.templates { | 437 | rect[2] - rect[0] - position[0] - 24.0, |
| 458 | View::Gallery => { | 438 | ); |
| 459 | let size = [rect[2] - rect[0], rect[3] - rect[1]]; | ||
| 460 | gallery( | ||
| 461 | &mut self.ui, | ||
| 462 | theme, | ||
| 463 | &mut self.thumbnails, | ||
| 464 | paper, | ||
| 465 | dark, | ||
| 466 | size, | ||
| 467 | false, | ||
| 468 | ) | ||
| 469 | } | ||
| 470 | View::Art => unreachable!("The art gallery is built alone"), | ||
| 471 | view => strip( | ||
| 472 | &mut self.ui, | ||
| 473 | theme, | ||
| 474 | &mut self.thumbnails, | ||
| 475 | paper, | ||
| 476 | dark, | ||
| 477 | position, | ||
| 478 | rect[2] - rect[0] - position[0] - 24.0, | ||
| 479 | view == View::Colors, | ||
| 480 | ), | ||
| 481 | }; | ||
| 482 | match chosen { | 439 | match chosen { |
| 483 | Some(Choice::More) => self.templates = View::Gallery, | 440 | Some(Choice::Template(name)) => self.commands.push(crate::Command::Template(name)), |
| 484 | Some(Choice::Colors) => { | 441 | Some(Choice::Colors) => self.ui.open_popup(colors()), |
| 485 | self.templates = if self.templates == View::Colors { | 442 | Some(Choice::More) => self.ui.open_popup(templates()), |
| 486 | View::Strip | ||
| 487 | } else { | ||
| 488 | View::Colors | ||
| 489 | } | ||
| 490 | } | ||
| 491 | Some(Choice::Dismiss) if self.templates == View::Gallery => { | ||
| 492 | self.templates = View::Strip | ||
| 493 | } | ||
| 494 | Some(Choice::Dismiss) => { | 443 | Some(Choice::Dismiss) => { |
| 495 | self.dismissed.insert(space); | 444 | self.dismissed.insert(space); |
| 496 | } | 445 | } |
| 497 | Some(choice) => { | ||
| 498 | self.templates = View::Strip; | ||
| 499 | self.commands.push(crate::Command::Template(choice)); | ||
| 500 | } | ||
| 501 | None => {} | 446 | None => {} |
| 502 | } | 447 | } |
| 503 | } | 448 | if let Some(name) = gallery( |
| 504 | |||
| 505 | /// The Page Color menu's Show All: every template's art, one of which goes behind the | ||
| 506 | /// open page. | ||
| 507 | fn art_gallery(&mut self, theme: &Theme) { | ||
| 508 | let Some(rect) = self.ui.laid_out(crate::page()) else { | ||
| 509 | return; | ||
| 510 | }; | ||
| 511 | let paper = Paper { | ||
| 512 | color: self.ui.theme.paper, | ||
| 513 | ink: self.ui.theme.paper_ink, | ||
| 514 | }; | ||
| 515 | let dark = paper.ink[0] > paper.color[0]; | ||
| 516 | let size = [rect[2] - rect[0], rect[3] - rect[1]]; | ||
| 517 | match gallery( | ||
| 518 | &mut self.ui, | 449 | &mut self.ui, |
| 519 | theme, | 450 | theme, |
| 520 | &mut self.thumbnails, | 451 | &mut self.thumbnails, |
| 521 | paper, | 452 | paper, |
| 522 | dark, | 453 | templates(), |
| 523 | size, | 454 | "Page Templates", |
| 524 | true, | 455 | height, |
| 525 | ) { | 456 | ) { |
| 526 | Some(Choice::Template(name)) => { | 457 | self.commands.push(crate::Command::Template(name)); |
| 527 | self.templates = View::Strip; | 458 | } |
| 528 | self.choose(crate::commands::Choice::Art(Some(name))); | 459 | if let Some(choice) = solid.and_then(|tile| { |
| 529 | } | 460 | crate::background::menu( |
| 530 | Some(Choice::Dismiss) => self.templates = View::Strip, | 461 | &mut self.ui, |
| 531 | _ => {} | 462 | colors(), |
| 463 | Anchor::Below(tile), | ||
| 464 | &mut self.thumbnails, | ||
| 465 | paper, | ||
| 466 | color, | ||
| 467 | rule_lines, | ||
| 468 | true, | ||
| 469 | &mut self.page_color_preview, | ||
| 470 | ) | ||
| 471 | }) { | ||
| 472 | self.choose(choice); | ||
| 532 | } | 473 | } |
| 533 | } | 474 | } |
| 534 | } | 475 | } |
crates/ui/src/layout.rs+6-1| ... | @@ -62,7 +62,11 @@ fn resolve(nodes: &mut [Built], popup: &[usize], index: usize, states: &HashMap< | ... | @@ -62,7 +62,11 @@ fn resolve(nodes: &mut [Built], popup: &[usize], index: usize, states: &HashMap< |
| 62 | let around = match anchor { | 62 | let around = match anchor { |
| 63 | Anchor::Point([x, y]) => [x, y, x, y], | 63 | Anchor::Point([x, y]) => [x, y, x, y], |
| 64 | Anchor::Dialog | Anchor::Top => [0.0; 4], | 64 | Anchor::Dialog | Anchor::Top => [0.0; 4], |
| 65 | Anchor::Below(id) | Anchor::Tip(id) | Anchor::Right(id) | Anchor::Over(id) => { | 65 | Anchor::Below(id) |
| 66 | | Anchor::Tip(id) | ||
| 67 | | Anchor::Right(id) | ||
| 68 | | Anchor::Over(id) | ||
| 69 | | Anchor::Dock(id) => { | ||
| 66 | let (rect, holder) = laid_out(id).unwrap_or_default(); | 70 | let (rect, holder) = laid_out(id).unwrap_or_default(); |
| 67 | let [left, top, right, bottom] = rect; | 71 | let [left, top, right, bottom] = rect; |
| 68 | match anchor { | 72 | match anchor { |
| ... | @@ -75,6 +79,7 @@ fn resolve(nodes: &mut [Built], popup: &[usize], index: usize, states: &HashMap< | ... | @@ -75,6 +79,7 @@ fn resolve(nodes: &mut [Built], popup: &[usize], index: usize, states: &HashMap< |
| 75 | [left, top - pad_y, right, bottom + pad_y] | 79 | [left, top - pad_y, right, bottom + pad_y] |
| 76 | } | 80 | } |
| 77 | Anchor::Over(_) => [left - pad_x, top - pad_y, right + pad_x, bottom + pad_y], | 81 | Anchor::Over(_) => [left - pad_x, top - pad_y, right + pad_x, bottom + pad_y], |
| 82 | Anchor::Dock(_) => rect, | ||
| 78 | _ => [left, top - PAD, right, bottom + PAD], | 83 | _ => [left, top - PAD, right, bottom + PAD], |
| 79 | } | 84 | } |
| 80 | } | 85 | } |
crates/ui/src/lib.rs+12-3| ... | @@ -240,6 +240,9 @@ pub enum Anchor { | ... | @@ -240,6 +240,9 @@ pub enum Anchor { |
| 240 | /// Where a dialog opens, swinging in as one does but leaving the interface undimmed, as a | 240 | /// Where a dialog opens, swinging in as one does but leaving the interface undimmed, as a |
| 241 | /// command palette opens. | 241 | /// command palette opens. |
| 242 | Top, | 242 | Top, |
| 243 | /// Inside the box along its trailing edge, a dialog leaving the rest of the box in view and | ||
| 244 | /// the interface undimmed, as a side panel docks. | ||
| 245 | Dock(Id), | ||
| 243 | } | 246 | } |
| 244 | 247 | ||
| 245 | impl Anchor { | 248 | impl Anchor { |
| ... | @@ -262,6 +265,10 @@ impl Anchor { | ... | @@ -262,6 +265,10 @@ impl Anchor { |
| 262 | return ((room - size) / 2.0).max(POPUP_MARGIN); | 265 | return ((room - size) / 2.0).max(POPUP_MARGIN); |
| 263 | } | 266 | } |
| 264 | Anchor::Dialog | Anchor::Top => return (room / 8.0).clamp(POPUP_MARGIN, most), | 267 | Anchor::Dialog | Anchor::Top => return (room / 8.0).clamp(POPUP_MARGIN, most), |
| 268 | Anchor::Dock(_) if axis == 0 => { | ||
| 269 | return (around[2] - size - POPUP_MARGIN).clamp(POPUP_MARGIN, most); | ||
| 270 | } | ||
| 271 | Anchor::Dock(_) => return (around[1] + POPUP_MARGIN).clamp(POPUP_MARGIN, most), | ||
| 265 | }; | 272 | }; |
| 266 | let [low, high] = [around[axis], around[axis + 2]]; | 273 | let [low, high] = [around[axis], around[axis + 2]]; |
| 267 | let (first, second) = if along == Some(axis) { | 274 | let (first, second) = if along == Some(axis) { |
| ... | @@ -280,7 +287,7 @@ impl Anchor { | ... | @@ -280,7 +287,7 @@ impl Anchor { |
| 280 | 287 | ||
| 281 | /// Seconds the popup takes to open and to close. | 288 | /// Seconds the popup takes to open and to close. |
| 282 | fn durations(self) -> [f32; 2] { | 289 | fn durations(self) -> [f32; 2] { |
| 283 | if matches!(self, Anchor::Dialog | Anchor::Top) { | 290 | if matches!(self, Anchor::Dialog | Anchor::Top | Anchor::Dock(_)) { |
| 284 | DIALOG | 291 | DIALOG |
| 285 | } else { | 292 | } else { |
| 286 | POPUP | 293 | POPUP |
| ... | @@ -603,10 +610,12 @@ impl Placed { | ... | @@ -603,10 +610,12 @@ impl Placed { |
| 603 | Anchor::Right(_) => [around[2], around[1]], | 610 | Anchor::Right(_) => [around[2], around[1]], |
| 604 | Anchor::Over(_) => [around[0], around[1]], | 611 | Anchor::Over(_) => [around[0], around[1]], |
| 605 | Anchor::Dialog | Anchor::Top => [(rect[0] + rect[2]) / 2.0, rect[1]], | 612 | Anchor::Dialog | Anchor::Top => [(rect[0] + rect[2]) / 2.0, rect[1]], |
| 613 | Anchor::Dock(_) => [rect[2], (rect[1] + rect[3]) / 2.0], | ||
| 606 | }; | 614 | }; |
| 607 | let (from, tilt) = match anchor { | 615 | let (from, tilt) = match anchor { |
| 608 | _ if !grows => (1.0, 0.0), | 616 | _ if !grows => (1.0, 0.0), |
| 609 | Anchor::Dialog | Anchor::Top => (0.95, 0.2), | 617 | Anchor::Dialog | Anchor::Top => (0.95, 0.2), |
| 618 | Anchor::Dock(_) => (0.95, 0.0), | ||
| 610 | // Over a box, the popup widens out of it in layout instead of growing. | 619 | // Over a box, the popup widens out of it in layout instead of growing. |
| 611 | Anchor::Over(_) => (1.0, 0.2), | 620 | Anchor::Over(_) => (1.0, 0.2), |
| 612 | _ => (0.94, 0.2), | 621 | _ => (0.94, 0.2), |
| ... | @@ -1573,7 +1582,7 @@ impl Ui { | ... | @@ -1573,7 +1582,7 @@ impl Ui { |
| 1573 | /// How popups beside `anchor` open and close; dialogs and the palette always swing as | 1582 | /// How popups beside `anchor` open and close; dialogs and the palette always swing as |
| 1574 | /// Snowbound's own. | 1583 | /// Snowbound's own. |
| 1575 | fn popup_motion(&self, anchor: Anchor) -> PopupMotion { | 1584 | fn popup_motion(&self, anchor: Anchor) -> PopupMotion { |
| 1576 | if matches!(anchor, Anchor::Dialog | Anchor::Top) { | 1585 | if matches!(anchor, Anchor::Dialog | Anchor::Top | Anchor::Dock(_)) { |
| 1577 | PopupMotion::Grow | 1586 | PopupMotion::Grow |
| 1578 | } else { | 1587 | } else { |
| 1579 | self.theme.menu().motion | 1588 | self.theme.menu().motion |
| ... | @@ -1650,7 +1659,7 @@ impl Ui { | ... | @@ -1650,7 +1659,7 @@ impl Ui { |
| 1650 | if let Some(color) = node.shadow { | 1659 | if let Some(color) = node.shadow { |
| 1651 | let (own, menu); | 1660 | let (own, menu); |
| 1652 | let shadows: &[Shadow] = match node.anchor { | 1661 | let shadows: &[Shadow] = match node.anchor { |
| 1653 | Some(Anchor::Dialog) | None => { | 1662 | Some(Anchor::Dialog | Anchor::Dock(_)) | None => { |
| 1654 | let [blur, drop] = if node.anchor.is_some() { | 1663 | let [blur, drop] = if node.anchor.is_some() { |
| 1655 | POPUP_SHADOW | 1664 | POPUP_SHADOW |
| 1656 | } else { | 1665 | } else { |
crates/ui/src/popup.rs+24-1| ... | @@ -890,7 +890,8 @@ fn highlighted(ui: &mut Ui, id: Id, cell: Option<Id>) { | ... | @@ -890,7 +890,8 @@ fn highlighted(ui: &mut Ui, id: Id, cell: Option<Id>) { |
| 890 | } | 890 | } |
| 891 | 891 | ||
| 892 | /// Routes the pointer and keys over open popup `id`'s `count` cells, keeping its highlight, | 892 | /// Routes the pointer and keys over open popup `id`'s `count` cells, keeping its highlight, |
| 893 | /// which `step` moves for each arrow. Returns the highlight and the cell a click or Enter chose. | 893 | /// which `step` moves for each arrow and the pointer leaving the popup clears. Returns the |
| 894 | /// highlight and the cell a click or Enter chose. | ||
| 894 | fn pick_cell( | 895 | fn pick_cell( |
| 895 | ui: &mut Ui, | 896 | ui: &mut Ui, |
| 896 | id: Id, | 897 | id: Id, |
| ... | @@ -910,6 +911,15 @@ fn pick_cell( | ... | @@ -910,6 +911,15 @@ fn pick_cell( |
| 910 | ], | 911 | ], |
| 911 | ); | 912 | ); |
| 912 | let mut highlight = state(ui, id).highlight.map(|cell| cell as usize); | 913 | let mut highlight = state(ui, id).highlight.map(|cell| cell as usize); |
| 914 | let outside = | ||
| 915 | ui.pointer() | ||
| 916 | .zip(ui.laid_out(id)) | ||
| 917 | .is_some_and(|([x, y], [left, top, right, bottom])| { | ||
| 918 | x < left || x >= right || y < top || y >= bottom | ||
| 919 | }); | ||
| 920 | if outside && ui.moved { | ||
| 921 | highlight = None; | ||
| 922 | } | ||
| 913 | let mut chosen = None; | 923 | let mut chosen = None; |
| 914 | for index in 0..count { | 924 | for index in 0..count { |
| 915 | let signal = ui.signal(cell(index)); | 925 | let signal = ui.signal(cell(index)); |
| ... | @@ -930,6 +940,13 @@ fn pick_cell( | ... | @@ -930,6 +940,13 @@ fn pick_cell( |
| 930 | (highlight, chosen) | 940 | (highlight, chosen) |
| 931 | } | 941 | } |
| 932 | 942 | ||
| 943 | /// The cell open popup `id`, a grid or gallery, highlights: under the pointer or reached by | ||
| 944 | /// the arrows. | ||
| 945 | pub fn highlight(ui: &Ui, id: Id) -> Option<usize> { | ||
| 946 | let popup = ui.popups.iter().find(|popup| popup.id == id)?; | ||
| 947 | popup.highlight.map(|cell| cell as usize) | ||
| 948 | } | ||
| 949 | |||
| 933 | /// Builds popup `id` as a grid of `size` columns and rows beside `anchor` while it is open, | 950 | /// Builds popup `id` as a grid of `size` columns and rows beside `anchor` while it is open, |
| 934 | /// lit from its corner to the cell pointed at, as Office's table picker is. Returns the | 951 | /// lit from its corner to the cell pointed at, as Office's table picker is. Returns the |
| 935 | /// columns and rows chosen. | 952 | /// columns and rows chosen. |
| ... | @@ -1369,6 +1386,12 @@ pub fn color_picker( | ... | @@ -1369,6 +1386,12 @@ pub fn color_picker( |
| 1369 | None | 1386 | None |
| 1370 | } | 1387 | } |
| 1371 | 1388 | ||
| 1389 | /// The colour open colour picker `id` shows, sRGB, before it is applied. | ||
| 1390 | pub fn picking(ui: &Ui, id: Id) -> Option<[u8; 3]> { | ||
| 1391 | let popup = ui.popups.iter().find(|popup| popup.id == id)?; | ||
| 1392 | popup.picked.map(from_hsl) | ||
| 1393 | } | ||
| 1394 | |||
| 1372 | /// Hue in degrees, saturation and lightness of an sRGB colour. | 1395 | /// Hue in degrees, saturation and lightness of an sRGB colour. |
| 1373 | fn to_hsl(rgb: [u8; 3]) -> [f32; 3] { | 1396 | fn to_hsl(rgb: [u8; 3]) -> [f32; 3] { |
| 1374 | let [red, green, blue] = rgb.map(|channel| f32::from(channel) / 255.0); | 1397 | let [red, green, blue] = rgb.map(|channel| f32::from(channel) / 255.0); |
crates/ui/src/tests.rs+54| ... | @@ -1840,6 +1840,60 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() { | ... | @@ -1840,6 +1840,60 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() { |
| 1840 | assert_eq!(build(&mut ui), Some(None)); | 1840 | assert_eq!(build(&mut ui), Some(None)); |
| 1841 | } | 1841 | } |
| 1842 | 1842 | ||
| 1843 | #[test] | ||
| 1844 | fn colour_grids_highlight_the_swatch_pointed_at_until_the_pointer_leaves() { | ||
| 1845 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | ||
| 1846 | let grid = Id::ROOT.child("colours"); | ||
| 1847 | let swatches = [([1.0, 0.0, 0.0, 1.0], ""), ([0.0, 1.0, 0.0, 1.0], "")]; | ||
| 1848 | let build = |ui: &mut Ui| { | ||
| 1849 | frame(ui, |ui| { | ||
| 1850 | let anchor = below(ui); | ||
| 1851 | popup::colors(ui, grid, anchor, "Automatic", &swatches, 2); | ||
| 1852 | }); | ||
| 1853 | }; | ||
| 1854 | build(&mut ui); | ||
| 1855 | ui.open_popup(grid); | ||
| 1856 | for _ in 0..40 { | ||
| 1857 | build(&mut ui); | ||
| 1858 | } | ||
| 1859 | let rect = ui.laid_out(grid.child(("cell", 2_usize))).unwrap(); | ||
| 1860 | ui.event(Event::PointerMoved([rect[0] + 4.0, rect[1] + 4.0])); | ||
| 1861 | build(&mut ui); | ||
| 1862 | assert_eq!(popup::highlight(&ui, grid), Some(2)); | ||
| 1863 | ui.event(Event::PointerMoved([399.0, 299.0])); | ||
| 1864 | build(&mut ui); | ||
| 1865 | assert_eq!(popup::highlight(&ui, grid), None); | ||
| 1866 | } | ||
| 1867 | |||
| 1868 | #[test] | ||
| 1869 | fn docked_popups_lie_inside_their_box_along_its_trailing_edge() { | ||
| 1870 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | ||
| 1871 | let dock = Id::ROOT.child("dock"); | ||
| 1872 | let build = |ui: &mut Ui| { | ||
| 1873 | frame(ui, |ui| { | ||
| 1874 | let page = spot(ui, [20.0, 40.0, 380.0, 290.0]); | ||
| 1875 | if ui.popup_open(dock) { | ||
| 1876 | ui.open_as( | ||
| 1877 | dock, | ||
| 1878 | Spec { | ||
| 1879 | size: [px(100.0), px(200.0)], | ||
| 1880 | anchor: Some(Anchor::Dock(page)), | ||
| 1881 | ..Spec::default() | ||
| 1882 | }, | ||
| 1883 | ); | ||
| 1884 | ui.close(); | ||
| 1885 | } | ||
| 1886 | }); | ||
| 1887 | }; | ||
| 1888 | build(&mut ui); | ||
| 1889 | ui.open_popup(dock); | ||
| 1890 | for _ in 0..40 { | ||
| 1891 | build(&mut ui); | ||
| 1892 | } | ||
| 1893 | let [left, top, right, bottom] = ui.laid_out(dock).unwrap(); | ||
| 1894 | assert_eq!([left, top, right, bottom], [276.0, 44.0, 376.0, 244.0]); | ||
| 1895 | } | ||
| 1896 | |||
| 1843 | #[test] | 1897 | #[test] |
| 1844 | fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() { | 1898 | fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() { |
| 1845 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | 1899 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); |