| 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 | 27 | slow enough to follow: a menu swings out of the pointer or its button as it |
| 28 | 28 | fades in, a combo's field widens into its list, and a dialog swings up into |
| 29 | 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 | 32 | results at once. On GNOME and KDE, menus instead |
| 32 | 33 | look and move as the desktop's own (see [platforms](platforms.md)). A segmented control |
| 33 | 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 | 4 | use crate::{ |
| 5 | 5 | art, |
| 6 | 6 | commands::Choice, |
| 7 | templates::{TILE, Thumbnails, View}, | |
| 7 | templates::{TILE, Thumbnails}, | |
| 8 | 8 | }; |
| 9 | 9 | use canvas::{ |
| 10 | 10 | gpu::{Paper, colorref}, |
| ... | ... | @@ -42,7 +42,8 @@ enum Cell { |
| 42 | 42 | |
| 43 | 43 | /// Builds popup `id` beside `anchor` while it is open, for a page on `paper` (its own colour |
| 44 | 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 | 47 | /// choice made. |
| 47 | 48 | #[allow(clippy::too_many_arguments)] |
| 48 | 49 | pub fn menu( |
| ... | ... | @@ -50,10 +51,11 @@ pub fn menu( |
| 50 | 51 | id: Id, |
| 51 | 52 | anchor: Anchor, |
| 52 | 53 | thumbnails: &mut Thumbnails, |
| 53 | view: &mut View, | |
| 54 | 54 | paper: Paper, |
| 55 | 55 | color: Option<u32>, |
| 56 | 56 | rule_lines: Option<RuleLines>, |
| 57 | colors_only: bool, | |
| 58 | preview: &mut Option<Option<u32>>, | |
| 57 | 59 | ) -> Option<Choice> { |
| 58 | 60 | let picker = id.child("custom"); |
| 59 | 61 | // A pale blue to start from on a page without a colour. |
| ... | ... | @@ -74,6 +76,9 @@ pub fn menu( |
| 74 | 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 | 82 | let custom = color.filter(|color| !PAGE_COLORS.iter().any(|(_, listed)| listed == color)); |
| 78 | 83 | let cells: Vec<Cell> = std::iter::once(Cell::Color(None)) |
| 79 | 84 | .chain( |
| ... | ... | @@ -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 | 158 | let name = match cells[index] { |
| 153 | 159 | Cell::Color(Some(color)) => PAGE_COLORS |
| 154 | 160 | .iter() |
| ... | ... | @@ -236,7 +242,11 @@ pub fn menu( |
| 236 | 242 | } |
| 237 | 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 | 250 | match cells[chosen] { |
| 241 | 251 | Cell::Color(color) => Some(Choice::PageColor(color)), |
| 242 | 252 | Cell::Custom => { |
| ... | ... | @@ -245,7 +255,7 @@ pub fn menu( |
| 245 | 255 | } |
| 246 | 256 | Cell::Art(name) => Some(Choice::Art(name)), |
| 247 | 257 | Cell::ShowAll => { |
| 248 | *view = View::Art; | |
| 258 | ui.open_popup(crate::templates::backgrounds()); | |
| 249 | 259 | None |
| 250 | 260 | } |
| 251 | 261 | Cell::Rules(index) => Some(Choice::RuleLines(index)), |
crates/snowbound/src/main.rs+19-9| ... | ... | @@ -718,8 +718,8 @@ enum Command { |
| 718 | 718 | space: ExGuid, |
| 719 | 719 | path: String, |
| 720 | 720 | }, |
| 721 | /// Gives the open page a template's background or colour. | |
| 722 | Template(templates::Choice), | |
| 721 | /// Gives the open page a template's background. | |
| 722 | Template(&'static str), | |
| 723 | 723 | OpenPage(ExGuid), |
| 724 | 724 | /// Shows a page's conflict pages in the list and opens the newest, or hides them. |
| 725 | 725 | Versions { |
| ... | ... | @@ -902,8 +902,8 @@ struct State { |
| 902 | 902 | dismissed: HashSet<ExGuid>, |
| 903 | 903 | /// A page just created, whose title takes the caret once it opens. |
| 904 | 904 | title_focus: Option<ExGuid>, |
| 905 | /// What the template strip shows over a blank page. | |
| 906 | templates: templates::View, | |
| 905 | /// The page colour a menu previews over the open page's own this frame. | |
| 906 | page_color_preview: Option<Option<u32>>, | |
| 907 | 907 | media: recording::Media, |
| 908 | 908 | /// Whether playback highlights the notes linked to the moment playing. |
| 909 | 909 | see_playback: bool, |
| ... | ... | @@ -1349,7 +1349,7 @@ impl State { |
| 1349 | 1349 | .map(|id| notebook::sidecar::themes::successor(&id).to_owned()), |
| 1350 | 1350 | live_options: stored.live, |
| 1351 | 1351 | drag: None, |
| 1352 | templates: templates::View::Strip, | |
| 1352 | page_color_preview: None, | |
| 1353 | 1353 | media: Default::default(), |
| 1354 | 1354 | see_playback: true, |
| 1355 | 1355 | thumbnails: templates::Thumbnails::default(), |
| ... | ... | @@ -1560,7 +1560,12 @@ impl State { |
| 1560 | 1560 | fn layout(&mut self, size: [f32; 2], scale: f32) -> Result<(), Box<dyn Error>> { |
| 1561 | 1561 | self.ui.begin(size, scale, Instant::now()); |
| 1562 | 1562 | platform::cover_border_line(&mut self.ui, size[0]); |
| 1563 | let previewed = self.page_color_preview; | |
| 1563 | 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 | 1569 | self.options_dialog(); |
| 1565 | 1570 | self.themes_dialog(); |
| 1566 | 1571 | self.print_dialog(); |
| ... | ... | @@ -1677,6 +1682,7 @@ impl State { |
| 1677 | 1682 | // The page, and the open page's tab joined to it, take the page's colour. |
| 1678 | 1683 | let mut theme = self.ui.theme.clone(); |
| 1679 | 1684 | theme.paper = self.paper().color; |
| 1685 | self.page_color_preview = None; | |
| 1680 | 1686 | platform::update_menu(|| self.statuses()); |
| 1681 | 1687 | let welcome = self.session.is_none() |
| 1682 | 1688 | && !self.temporary |
| ... | ... | @@ -3049,7 +3055,6 @@ impl State { |
| 3049 | 3055 | menu, |
| 3050 | 3056 | anchor, |
| 3051 | 3057 | &mut self.thumbnails, |
| 3052 | &mut self.templates, | |
| 3053 | 3058 | // The page's own colour aside. |
| 3054 | 3059 | canvas::gpu::Paper { |
| 3055 | 3060 | color: ui.theme.paper, |
| ... | ... | @@ -3057,6 +3062,8 @@ impl State { |
| 3057 | 3062 | }, |
| 3058 | 3063 | self.view.editor.page_color(), |
| 3059 | 3064 | self.view.editor.rule_lines(), |
| 3065 | false, | |
| 3066 | &mut self.page_color_preview, | |
| 3060 | 3067 | ) |
| 3061 | 3068 | }, |
| 3062 | 3069 | |_| None, |
| ... | ... | @@ -3624,7 +3631,7 @@ impl State { |
| 3624 | 3631 | self.move_page(space, path)?; |
| 3625 | 3632 | self.edited(Vec::new()); |
| 3626 | 3633 | } |
| 3627 | Command::Template(choice) => self.apply_template(choice)?, | |
| 3634 | Command::Template(name) => self.apply_template(name)?, | |
| 3628 | 3635 | Command::Page(Request::EditDate(field)) => self.edit_date(field), |
| 3629 | 3636 | Command::Page(Request::Copy(clip)) => self.clipboard.set(clip)?, |
| 3630 | 3637 | Command::Page(Request::Paste) => self.paste()?, |
| ... | ... | @@ -4557,13 +4564,16 @@ impl State { |
| 4557 | 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 | 4568 | fn paper(&self) -> canvas::gpu::Paper { |
| 4562 | 4569 | canvas::gpu::Paper { |
| 4563 | 4570 | color: self.ui.theme.paper, |
| 4564 | 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 | 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 | 180 | Ok(()) |
| 181 | 181 | } |
| 182 | 182 | |
| 183 | /// Gives the open page `choice`'s background: a template's art in place of any it | |
| 184 | /// had, or a page colour instead of art. | |
| 185 | pub(crate) fn apply_template( | |
| 186 | &mut self, | |
| 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| { | |
| 183 | /// Gives the open page template `name`'s art in place of any it had. | |
| 184 | pub(crate) fn apply_template(&mut self, name: &'static str) -> Result<(), Box<dyn Error>> { | |
| 185 | let template = canvas::template::find(name).ok_or("That template is not available")?; | |
| 186 | self.with_art(Some(template), move |state, art| { | |
| 197 | 187 | state.persist()?; |
| 198 | 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 | 190 | state.edit_page(ops) |
| 201 | 191 | }) |
| 202 | 192 | } |
| ... | ... | @@ -561,7 +551,6 @@ impl State { |
| 561 | 551 | self.persist().unwrap_or_else(|error| eprintln!("{error}")); |
| 562 | 552 | self.session = None; |
| 563 | 553 | self.sectionless = None; |
| 564 | self.templates = crate::templates::View::Strip; | |
| 565 | 554 | match self |
| 566 | 555 | .notebooks |
| 567 | 556 | .iter() |
| ... | ... | @@ -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 | |
| 970 | /// place of the background it had. | |
| 958 | /// The ops giving `page` template `name`'s art, `art`, in place of the background it had, | |
| 959 | /// and no page colour. | |
| 971 | 960 | pub fn template_ops( |
| 972 | 961 | page: &Page, |
| 973 | choice: crate::templates::Choice, | |
| 962 | name: &str, | |
| 974 | 963 | art: Vec<Image>, |
| 975 | 964 | ) -> Result<Vec<PageOp>, Box<dyn Error>> { |
| 976 | use crate::templates::Choice; | |
| 977 | let mut ops = match choice { | |
| 978 | Choice::Template(_) | Choice::Color(_) => art_ops(page, art), | |
| 979 | Choice::More | Choice::Dismiss | Choice::Colors => return Ok(Vec::new()), | |
| 980 | }; | |
| 981 | match choice { | |
| 982 | Choice::Template(name) => { | |
| 983 | if page.color.is_some() { | |
| 984 | ops.push(PageOp::Color(None)); | |
| 985 | } | |
| 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 => {} | |
| 965 | let mut ops = art_ops(page, art); | |
| 966 | if page.color.is_some() { | |
| 967 | ops.push(PageOp::Color(None)); | |
| 968 | } | |
| 969 | // The one template with content worth keeping. | |
| 970 | if name == "Informal Meeting Notes" { | |
| 971 | ops.extend(onestore::op::lower_page( | |
| 972 | page, | |
| 973 | &crate::meeting::content(page)?, | |
| 974 | )?); | |
| 998 | 975 | } |
| 999 | 976 | Ok(ops) |
| 1000 | 977 | } |
| ... | ... | @@ -1031,7 +1008,6 @@ fn art_ops(page: &Page, art: Vec<Image>) -> Vec<PageOp> { |
| 1031 | 1008 | #[cfg(test)] |
| 1032 | 1009 | mod tests { |
| 1033 | 1010 | use super::*; |
| 1034 | use crate::templates::Choice; | |
| 1035 | 1011 | use std::path::Path; |
| 1036 | 1012 | |
| 1037 | 1013 | const AUTHOR: &str = "Rust Author"; |
| ... | ... | @@ -1153,27 +1129,27 @@ mod tests { |
| 1153 | 1129 | let [ivy, teal, subpage, deleted, meeting] = spaces[..] else { |
| 1154 | 1130 | unreachable!() |
| 1155 | 1131 | }; |
| 1156 | let background = |space: ExGuid, choice: Choice| { | |
| 1157 | let art = match choice { | |
| 1158 | Choice::Template(name) => canvas::template::find(name).unwrap().pictures().unwrap(), | |
| 1159 | _ => Vec::new(), | |
| 1160 | }; | |
| 1161 | template_ops(&page(&file, space), choice, art) | |
| 1132 | let background = |space: ExGuid, name: &str| { | |
| 1133 | let art = canvas::template::find(name).unwrap().pictures().unwrap(); | |
| 1134 | template_ops(&page(&file, space), name, art) | |
| 1162 | 1135 | .unwrap() |
| 1163 | 1136 | .into_iter() |
| 1164 | 1137 | .map(|op| Op::Page { space, op }) |
| 1165 | 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 | 1146 | edit( |
| 1169 | 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 | 1153 | edit( |
| 1178 | 1154 | &file, |
| 1179 | 1155 | vec![Op::Section(SectionOp::Pages(vec![ |
| ... | ... | @@ -1275,10 +1251,7 @@ mod tests { |
| 1275 | 1251 | ] |
| 1276 | 1252 | ); |
| 1277 | 1253 | let shown = page(&file, teal); |
| 1278 | assert_eq!( | |
| 1279 | shown.color, | |
| 1280 | Some(canvas::template::PAGE_COLORS[teal_index].1) | |
| 1281 | ); | |
| 1254 | assert_eq!(shown.color, Some(*teal_color)); | |
| 1282 | 1255 | assert!(shown.date_text().is_some()); |
| 1283 | 1256 | assert!( |
| 1284 | 1257 | page(&file, ivy) |
crates/snowbound/src/print.rs+9-6| ... | ... | @@ -530,12 +530,15 @@ mod tests { |
| 530 | 530 | }; |
| 531 | 531 | let create = |creation| Op::Section(SectionOp::Create(creation)); |
| 532 | 532 | (section.apply("Author", edit(vec![create(dated), create(written)]))).unwrap(); |
| 533 | let meeting = crate::templates::Choice::Template("Informal Meeting Notes"); | |
| 534 | let ops = crate::manage::template_ops(&section.page(space).unwrap(), meeting, Vec::new()) | |
| 535 | .unwrap() | |
| 536 | .into_iter() | |
| 537 | .map(|op| Op::Page { space, op }) | |
| 538 | .collect(); | |
| 533 | let ops = crate::manage::template_ops( | |
| 534 | &section.page(space).unwrap(), | |
| 535 | "Informal Meeting Notes", | |
| 536 | Vec::new(), | |
| 537 | ) | |
| 538 | .unwrap() | |
| 539 | .into_iter() | |
| 540 | .map(|op| Op::Page { space, op }) | |
| 541 | .collect(); | |
| 539 | 542 | section.apply("Author", edit(ops)).unwrap(); |
| 540 | 543 | // The template titles the page; its title goes, its text stays. |
| 541 | 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 | 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 | 4 | use crate::art; |
| 5 | 5 | use canvas::{ |
| 6 | 6 | gpu::Paper, |
| 7 | template::{PAGE_COLORS, TEMPLATES, Template}, | |
| 7 | template::{TEMPLATES, Template}, | |
| 8 | 8 | }; |
| 9 | 9 | use draw::RasterImage; |
| 10 | 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 | 13 | /// What a thumbnail shows: the top of a letter page, in points from the page origin. |
| 14 | 14 | const REGION: [f32; 4] = [-72.0, -40.0, 540.0, 419.0]; |
| 15 | 15 | pub(crate) const TILE: [f32; 2] = [112.0, 84.0]; |
| 16 | const SWATCH: f32 = 24.0; | |
| 17 | 16 | const LABEL: f32 = 22.0; |
| 18 | 17 | const GAP: f32 = 12.0; |
| 19 | 18 | const PAD: f32 = 10.0; |
| ... | ... | @@ -30,26 +29,28 @@ const STRIP: [(Choice, &str); 5] = [ |
| 30 | 29 | ]; |
| 31 | 30 | |
| 32 | 31 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 33 | pub enum Choice { | |
| 32 | enum Choice { | |
| 34 | 33 | Template(&'static str), |
| 35 | /// An index into `PAGE_COLORS`. | |
| 36 | Color(usize), | |
| 37 | /// Shows or hides the page colours. | |
| 34 | /// Opens the Page Color menu over its tile. | |
| 38 | 35 | Colors, |
| 39 | 36 | More, |
| 40 | 37 | Dismiss, |
| 41 | 38 | } |
| 42 | 39 | |
| 43 | /// What the template strip shows. | |
| 44 | #[derive(Clone, Copy, Debug, Default, PartialEq)] | |
| 45 | pub enum View { | |
| 46 | #[default] | |
| 47 | Strip, | |
| 48 | /// The strip with the page colours beneath it. | |
| 49 | Colors, | |
| 50 | Gallery, | |
| 51 | /// Every template's art, from the Page Color menu, to lie behind the open page. | |
| 52 | Art, | |
| 40 | /// The gallery of every template's art, from the Page Color menu's Show All, to lie behind | |
| 41 | /// the open page. | |
| 42 | pub fn backgrounds() -> Id { | |
| 43 | Id::ROOT.child("backgrounds") | |
| 44 | } | |
| 45 | ||
| 46 | /// The gallery of every template, from the strip's More templates. | |
| 47 | fn templates() -> Id { | |
| 48 | Id::ROOT.child("templates") | |
| 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 | 56 | /// A thumbnail's art: each raster and where it lies in the tile. |
| ... | ... | @@ -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 | /// beneath while `colors`. Picks that do not fit give way from the end; More templates | |
| 92 | /// stays. | |
| 93 | #[allow(clippy::too_many_arguments)] | |
| 91 | /// The strip at `position` in the page box, `room` wide at most. Picks that do not fit give | |
| 92 | /// way from the end; More templates stays. Returns the pick and the Solid Color tile, where | |
| 93 | /// shown. | |
| 94 | 94 | fn strip( |
| 95 | 95 | ui: &mut Ui, |
| 96 | 96 | theme: &Theme, |
| 97 | 97 | thumbnails: &mut Thumbnails, |
| 98 | 98 | paper: Paper, |
| 99 | dark: bool, | |
| 100 | 99 | position: [f32; 2], |
| 101 | 100 | room: f32, |
| 102 | colors: bool, | |
| 103 | ) -> Option<Choice> { | |
| 101 | ) -> (Option<Choice>, Option<Id>) { | |
| 104 | 102 | let more = (Choice::More, "More templates"); |
| 105 | 103 | let mut used = 2.0 * PAD + tile_width(ui, more.1); |
| 106 | 104 | let mut shown = Vec::new(); |
| ... | ... | @@ -117,6 +115,7 @@ fn strip( |
| 117 | 115 | .sum::<f32>() |
| 118 | 116 | - GAP; |
| 119 | 117 | let mut chosen = None; |
| 118 | let mut solid = None; | |
| 120 | 119 | ui.open( |
| 121 | 120 | "templates", |
| 122 | 121 | Spec { |
| ... | ... | @@ -128,7 +127,6 @@ fn strip( |
| 128 | 127 | shadow: Some(theme.shadow), |
| 129 | 128 | radius: 8.0, |
| 130 | 129 | pad: [PAD, PAD], |
| 131 | gap: 8.0, | |
| 132 | 130 | ..Spec::default() |
| 133 | 131 | }, |
| 134 | 132 | ); |
| ... | ... | @@ -141,46 +139,16 @@ fn strip( |
| 141 | 139 | }, |
| 142 | 140 | ); |
| 143 | 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 | 148 | chosen = Some(choice); |
| 146 | 149 | } |
| 147 | 150 | } |
| 148 | 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 | 152 | let close = ui.leaf( |
| 185 | 153 | "dismiss", |
| 186 | 154 | Spec { |
| ... | ... | @@ -203,37 +171,45 @@ fn strip( |
| 203 | 171 | chosen = Some(Choice::Dismiss); |
| 204 | 172 | } |
| 205 | 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 | |
| 210 | /// `size` big. | |
| 177 | /// Dialog `id`, titled `title`, of every template docked inside the page box's trailing edge, | |
| 178 | /// `height` tall, while it is open. Returns the template chosen; it stays open to choose again. | |
| 179 | #[allow(clippy::too_many_arguments)] | |
| 211 | 180 | fn gallery( |
| 212 | 181 | ui: &mut Ui, |
| 213 | 182 | theme: &Theme, |
| 214 | 183 | thumbnails: &mut Thumbnails, |
| 215 | 184 | paper: Paper, |
| 216 | dark: bool, | |
| 217 | size: [f32; 2], | |
| 218 | art: bool, | |
| 219 | ) -> Option<Choice> { | |
| 185 | id: Id, | |
| 186 | title: &str, | |
| 187 | height: f32, | |
| 188 | ) -> Option<&'static str> { | |
| 189 | if !ui.popup_open(id) { | |
| 190 | return None; | |
| 191 | } | |
| 220 | 192 | let mut chosen = None; |
| 221 | let margin = 24.0; | |
| 222 | ui.open( | |
| 223 | "gallery", | |
| 193 | let column = TILE[0] + 40.0; | |
| 194 | ui.open_as( | |
| 195 | id, | |
| 224 | 196 | Spec { |
| 225 | flags: Flags::FLOAT | Flags::CLICKABLE, | |
| 226 | 197 | axis: Axis::Y, |
| 227 | size: [px(size[0] - 2.0 * margin), px(size[1] - 2.0 * margin)], | |
| 228 | position: [margin, margin], | |
| 198 | size: [px(2.0 * column + GAP + 2.0 * PAD), px(height)], | |
| 229 | 199 | fill: Some(theme.popup), |
| 200 | border: Some(theme.chip), | |
| 230 | 201 | shadow: Some(theme.shadow), |
| 231 | 202 | radius: 8.0, |
| 232 | 203 | pad: [PAD, PAD], |
| 233 | 204 | gap: 8.0, |
| 205 | anchor: Some(Anchor::Dock(crate::page())), | |
| 206 | role: Some(accesskit::Role::Dialog), | |
| 234 | 207 | ..Spec::default() |
| 235 | 208 | }, |
| 236 | 209 | ); |
| 210 | if let Some(node) = ui.access(id) { | |
| 211 | node.set_label(title); | |
| 212 | } | |
| 237 | 213 | ui.open( |
| 238 | 214 | "header", |
| 239 | 215 | Spec { |
| ... | ... | @@ -245,12 +221,14 @@ fn gallery( |
| 245 | 221 | "title", |
| 246 | 222 | Spec { |
| 247 | 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 | 227 | ..Spec::default() |
| 250 | 228 | }, |
| 251 | 229 | ); |
| 252 | 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 | 233 | crate::name(ui, ui.id("close"), "Close"); |
| 256 | 234 | ui.close(); |
| ... | ... | @@ -264,21 +242,7 @@ fn gallery( |
| 264 | 242 | ..Spec::default() |
| 265 | 243 | }, |
| 266 | 244 | ); |
| 267 | let column = TILE[0] + 40.0; | |
| 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() { | |
| 245 | for (row, templates) in TEMPLATES.chunks(2).enumerate() { | |
| 282 | 246 | ui.open( |
| 283 | 247 | row, |
| 284 | 248 | Spec { |
| ... | ... | @@ -287,9 +251,20 @@ fn gallery( |
| 287 | 251 | ..Spec::default() |
| 288 | 252 | }, |
| 289 | 253 | ); |
| 290 | for (index, (choice, label)) in choices.iter().enumerate() { | |
| 291 | if tile(ui, theme, thumbnails, paper, dark, index, *choice, label) { | |
| 292 | chosen = Some(*choice); | |
| 254 | for (index, template) in templates.iter().enumerate() { | |
| 255 | let choice = Choice::Template(template.name); | |
| 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 | 270 | ui.close(); |
| ... | ... | @@ -299,43 +274,26 @@ fn gallery( |
| 299 | 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 | 277 | /// How wide a tile labelled `label` stands: its thumbnail, or the label where it is wider. |
| 315 | 278 | fn tile_width(ui: &mut Ui, label: &str) -> f32 { |
| 316 | 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 | |
| 320 | /// when clicked. | |
| 282 | /// One template's thumbnail over its label, `width` wide; returns the thumbnail, which takes | |
| 283 | /// the click. | |
| 321 | 284 | #[allow(clippy::too_many_arguments)] |
| 322 | 285 | fn tile( |
| 323 | 286 | ui: &mut Ui, |
| 324 | 287 | theme: &Theme, |
| 325 | 288 | thumbnails: &mut Thumbnails, |
| 326 | 289 | paper: Paper, |
| 327 | dark: bool, | |
| 328 | 290 | part: usize, |
| 329 | 291 | choice: Choice, |
| 330 | 292 | label: &str, |
| 331 | ) -> bool { | |
| 293 | width: f32, | |
| 294 | ) -> Id { | |
| 332 | 295 | let scale = ui.scale(); |
| 333 | let width = tile_width(ui, label); | |
| 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 | }; | |
| 296 | let dark = paper.ink[0] > paper.color[0]; | |
| 339 | 297 | ui.open( |
| 340 | 298 | (part, label), |
| 341 | 299 | Spec { |
| ... | ... | @@ -350,7 +308,11 @@ fn tile( |
| 350 | 308 | Spec { |
| 351 | 309 | flags: Flags::CLICKABLE | Flags::CLIP, |
| 352 | 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 | 316 | border: Some(theme.chip), |
| 355 | 317 | hover_border: Some(theme.accent), |
| 356 | 318 | radius: 4.0, |
| ... | ... | @@ -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 | 349 | Choice::Colors => { |
| 388 | 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 | 353 | let band = TILE[0] / bands as f32; |
| 391 | 354 | ui.leaf( |
| 392 | 355 | ("band", index), |
| ... | ... | @@ -394,13 +357,13 @@ fn tile( |
| 394 | 357 | flags: Flags::FLOAT, |
| 395 | 358 | size: [px(band), px(TILE[1])], |
| 396 | 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 | 361 | ..Spec::default() |
| 399 | 362 | }, |
| 400 | 363 | ); |
| 401 | 364 | } |
| 402 | 365 | } |
| 403 | _ => {} | |
| 366 | Choice::More | Choice::Dismiss => {} | |
| 404 | 367 | } |
| 405 | 368 | ui.close(); |
| 406 | 369 | ui.leaf( |
| ... | ... | @@ -414,121 +377,99 @@ fn tile( |
| 414 | 377 | }, |
| 415 | 378 | ); |
| 416 | 379 | ui.close(); |
| 417 | ui.signal(id).clicked | |
| 380 | id | |
| 418 | 381 | } |
| 419 | 382 | |
| 420 | 383 | impl crate::State { |
| 421 | /// Over a page with nothing typed in its body: the strip where the body would start, | |
| 422 | /// or the gallery it opened. | |
| 384 | /// The template galleries while open, and over a page with nothing typed in its body the | |
| 385 | /// strip where the body would start. | |
| 423 | 386 | pub(crate) fn template_strip(&mut self, theme: &Theme) { |
| 424 | 387 | let Some(session) = &self.session else { |
| 425 | 388 | return; |
| 426 | 389 | }; |
| 427 | if self.templates == View::Art { | |
| 428 | return self.art_gallery(theme); | |
| 429 | } | |
| 430 | 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 | 412 | let editor = &self.view.editor; |
| 432 | 413 | let blank = editor |
| 433 | 414 | .visible_outlines() |
| 434 | 415 | .chain(editor.caret_outline()) |
| 435 | 416 | .filter(|outline| !outline.title) |
| 436 | 417 | .all(canvas::editor::TextOutline::is_empty); |
| 437 | if !blank || session.read_only() || self.dismissed.contains(&space) { | |
| 438 | self.templates = View::Strip; | |
| 418 | if !blank || read_only || self.dismissed.contains(&space) { | |
| 439 | 419 | return; |
| 440 | 420 | } |
| 441 | let (Some(rect), Some(start)) = (self.ui.laid_out(crate::page()), editor.body_start()) | |
| 442 | else { | |
| 421 | let Some(start) = editor.body_start() else { | |
| 443 | 422 | return; |
| 444 | 423 | }; |
| 424 | let (color, rule_lines) = (editor.page_color(), editor.rule_lines()); | |
| 445 | 425 | let viewport = self.view.viewport; |
| 446 | 426 | let scale = self.ui.scale(); |
| 447 | 427 | let position = [ |
| 448 | 428 | (viewport.origin[0] + start[0] * viewport.scale) / scale, |
| 449 | 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. | |
| 452 | let paper = Paper { | |
| 453 | color: self.ui.theme.paper, | |
| 454 | ink: self.ui.theme.paper_ink, | |
| 455 | }; | |
| 456 | let dark = paper.ink[0] > paper.color[0]; | |
| 457 | let chosen = match self.templates { | |
| 458 | View::Gallery => { | |
| 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 | }; | |
| 431 | let (chosen, solid) = strip( | |
| 432 | &mut self.ui, | |
| 433 | theme, | |
| 434 | &mut self.thumbnails, | |
| 435 | paper, | |
| 436 | position, | |
| 437 | rect[2] - rect[0] - position[0] - 24.0, | |
| 438 | ); | |
| 482 | 439 | match chosen { |
| 483 | Some(Choice::More) => self.templates = View::Gallery, | |
| 484 | Some(Choice::Colors) => { | |
| 485 | self.templates = if self.templates == View::Colors { | |
| 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 | } | |
| 440 | Some(Choice::Template(name)) => self.commands.push(crate::Command::Template(name)), | |
| 441 | Some(Choice::Colors) => self.ui.open_popup(colors()), | |
| 442 | Some(Choice::More) => self.ui.open_popup(templates()), | |
| 494 | 443 | Some(Choice::Dismiss) => { |
| 495 | 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 | 446 | None => {} |
| 502 | 447 | } |
| 503 | } | |
| 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( | |
| 448 | if let Some(name) = gallery( | |
| 518 | 449 | &mut self.ui, |
| 519 | 450 | theme, |
| 520 | 451 | &mut self.thumbnails, |
| 521 | 452 | paper, |
| 522 | dark, | |
| 523 | size, | |
| 524 | true, | |
| 453 | templates(), | |
| 454 | "Page Templates", | |
| 455 | height, | |
| 525 | 456 | ) { |
| 526 | Some(Choice::Template(name)) => { | |
| 527 | self.templates = View::Strip; | |
| 528 | self.choose(crate::commands::Choice::Art(Some(name))); | |
| 529 | } | |
| 530 | Some(Choice::Dismiss) => self.templates = View::Strip, | |
| 531 | _ => {} | |
| 457 | self.commands.push(crate::Command::Template(name)); | |
| 458 | } | |
| 459 | if let Some(choice) = solid.and_then(|tile| { | |
| 460 | crate::background::menu( | |
| 461 | &mut self.ui, | |
| 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 | 62 | let around = match anchor { |
| 63 | 63 | Anchor::Point([x, y]) => [x, y, x, y], |
| 64 | 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 | 70 | let (rect, holder) = laid_out(id).unwrap_or_default(); |
| 67 | 71 | let [left, top, right, bottom] = rect; |
| 68 | 72 | match anchor { |
| ... | ... | @@ -75,6 +79,7 @@ fn resolve(nodes: &mut [Built], popup: &[usize], index: usize, states: &HashMap< |
| 75 | 79 | [left, top - pad_y, right, bottom + pad_y] |
| 76 | 80 | } |
| 77 | 81 | Anchor::Over(_) => [left - pad_x, top - pad_y, right + pad_x, bottom + pad_y], |
| 82 | Anchor::Dock(_) => rect, | |
| 78 | 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 | 240 | /// Where a dialog opens, swinging in as one does but leaving the interface undimmed, as a |
| 241 | 241 | /// command palette opens. |
| 242 | 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 | 248 | impl Anchor { |
| ... | ... | @@ -262,6 +265,10 @@ impl Anchor { |
| 262 | 265 | return ((room - size) / 2.0).max(POPUP_MARGIN); |
| 263 | 266 | } |
| 264 | 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 | 273 | let [low, high] = [around[axis], around[axis + 2]]; |
| 267 | 274 | let (first, second) = if along == Some(axis) { |
| ... | ... | @@ -280,7 +287,7 @@ impl Anchor { |
| 280 | 287 | |
| 281 | 288 | /// Seconds the popup takes to open and to close. |
| 282 | 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 | 291 | DIALOG |
| 285 | 292 | } else { |
| 286 | 293 | POPUP |
| ... | ... | @@ -603,10 +610,12 @@ impl Placed { |
| 603 | 610 | Anchor::Right(_) => [around[2], around[1]], |
| 604 | 611 | Anchor::Over(_) => [around[0], around[1]], |
| 605 | 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 | 615 | let (from, tilt) = match anchor { |
| 608 | 616 | _ if !grows => (1.0, 0.0), |
| 609 | 617 | Anchor::Dialog | Anchor::Top => (0.95, 0.2), |
| 618 | Anchor::Dock(_) => (0.95, 0.0), | |
| 610 | 619 | // Over a box, the popup widens out of it in layout instead of growing. |
| 611 | 620 | Anchor::Over(_) => (1.0, 0.2), |
| 612 | 621 | _ => (0.94, 0.2), |
| ... | ... | @@ -1573,7 +1582,7 @@ impl Ui { |
| 1573 | 1582 | /// How popups beside `anchor` open and close; dialogs and the palette always swing as |
| 1574 | 1583 | /// Snowbound's own. |
| 1575 | 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 | 1586 | PopupMotion::Grow |
| 1578 | 1587 | } else { |
| 1579 | 1588 | self.theme.menu().motion |
| ... | ... | @@ -1650,7 +1659,7 @@ impl Ui { |
| 1650 | 1659 | if let Some(color) = node.shadow { |
| 1651 | 1660 | let (own, menu); |
| 1652 | 1661 | let shadows: &[Shadow] = match node.anchor { |
| 1653 | Some(Anchor::Dialog) | None => { | |
| 1662 | Some(Anchor::Dialog | Anchor::Dock(_)) | None => { | |
| 1654 | 1663 | let [blur, drop] = if node.anchor.is_some() { |
| 1655 | 1664 | POPUP_SHADOW |
| 1656 | 1665 | } else { |
crates/ui/src/popup.rs+24-1| ... | ... | @@ -890,7 +890,8 @@ fn highlighted(ui: &mut Ui, id: Id, cell: Option<Id>) { |
| 890 | 890 | } |
| 891 | 891 | |
| 892 | 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 | 895 | fn pick_cell( |
| 895 | 896 | ui: &mut Ui, |
| 896 | 897 | id: Id, |
| ... | ... | @@ -910,6 +911,15 @@ fn pick_cell( |
| 910 | 911 | ], |
| 911 | 912 | ); |
| 912 | 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 | 923 | let mut chosen = None; |
| 914 | 924 | for index in 0..count { |
| 915 | 925 | let signal = ui.signal(cell(index)); |
| ... | ... | @@ -930,6 +940,13 @@ fn pick_cell( |
| 930 | 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 | 950 | /// Builds popup `id` as a grid of `size` columns and rows beside `anchor` while it is open, |
| 934 | 951 | /// lit from its corner to the cell pointed at, as Office's table picker is. Returns the |
| 935 | 952 | /// columns and rows chosen. |
| ... | ... | @@ -1369,6 +1386,12 @@ pub fn color_picker( |
| 1369 | 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 | 1395 | /// Hue in degrees, saturation and lightness of an sRGB colour. |
| 1373 | 1396 | fn to_hsl(rgb: [u8; 3]) -> [f32; 3] { |
| 1374 | 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 | 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 | 1897 | #[test] |
| 1844 | 1898 | fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() { |
| 1845 | 1899 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); |