| author | |
| committer | |
| log | 074834705a828432d90e244347ec8f0913adb5e9 |
| tree | 6630a16557dd76cd7b9547291437085b0539f051 |
| parent | 9205754444f9fd5a6408f0268058de0dc5d9a3ac |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
- Toolbar (#17): every spec action wired through the command table, all 17 new
icons, table picker, 29 OneNote tags verified against a gallery OneNote wrote,
Format Painter, Picture, Screen Clipping, Symbol, Page Color, Full Page View.
- Sync status (#27): the top-right shows real sync state with OneNote 2010's
wording; popup with details, Work offline, and Sync Now.
- Select all (#38): a further select-all selects every outline, with OneNote
2010's delete, type, move, format, copy, and one-step undo; the title box
padding scales with zoom.
- UI kit: tooltips with shortcuts; combo and search popups widen with their
field; perspective on context menus; GNOME (libadwaita) and KDE (Breeze,
KWin fade) menus.
- Mac OS X 10.6 (#13): draw gains an OpenGL 2.1 backend beside wgpu, winit runs
over an AppKit shim, a 10.6 app bundle, and text weight matched to native.
Assisted-by: claude-opus-5.575 files changed, 6613 insertions(+), 1792 deletions(-)
arc/platforms.md+4| ... | @@ -59,6 +59,10 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. | ... | @@ -59,6 +59,10 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. |
| 59 | focused and not: on KDE the colour scheme's header colours from | 59 | focused and not: on KDE the colour scheme's header colours from |
| 60 | `kdeglobals`, as KWin paints its title bars, and on GNOME winit's Adwaita | 60 | `kdeglobals`, as KWin paints its title bars, and on GNOME winit's Adwaita |
| 61 | frame's. A settings portal signal re-reads them when the scheme changes. | 61 | frame's. A settings portal signal re-reads them when the scheme changes. |
| 62 | - Menus and other popups take the desktop's look and motion: libadwaita's | ||
| 63 | popover menus on GNOME, shown and hidden at once as GTK 4 does, and Breeze's | ||
| 64 | on KDE, faded as KWin fades popups and scaled by Plasma's animation speed. | ||
| 65 | The desktop is read once from `XDG_CURRENT_DESKTOP`; elsewhere the kit's own. | ||
| 62 | - zenity or kdialog provide the pickers and alerts. The XDG settings portal | 66 | - zenity or kdialog provide the pickers and alerts. The XDG settings portal |
| 63 | provides the colour scheme. Text conventions come from the C library's | 67 | provides the colour scheme. Text conventions come from the C library's |
| 64 | locale. Fontconfig is loaded at run time, so builds need no headers for it. | 68 | locale. Fontconfig is loaded at run time, so builds need no headers for it. |
arc/sync.md+3| ... | @@ -163,6 +163,9 @@ last listing lets it open while the server is unreachable, and a section opens | ... | @@ -163,6 +163,9 @@ last listing lets it open while the server is unreachable, and a section opens |
| 163 | from its replica. On reconnect the queue publishes, or rebases and publishes, | 163 | from its replica. On reconnect the queue publishes, or rebases and publishes, |
| 164 | through the same path as always. A failed directory read keeps the previous | 164 | through the same path as always. A failed directory read keeps the previous |
| 165 | catalog, so an unreachable share never looks like an emptied notebook. | 165 | catalog, so an unreachable share never looks like an emptied notebook. |
| 166 | Working offline on purpose, as OneNote's Work Offline does, is the same state | ||
| 167 | chosen: the sync thread stops stepping until Sync Now or until the user works | ||
| 168 | online again. | ||
| 166 | 169 | ||
| 167 | ## Notebook structure | 170 | ## Notebook structure |
| 168 | 171 |
arc/ui.md+2-1| ... | @@ -25,7 +25,8 @@ frame-rate independent, a retargeted animation continues smoothly from where | ... | @@ -25,7 +25,8 @@ frame-rate independent, a retargeted animation continues smoothly from where |
| 25 | it is, and it settles fast. Popups open and close on short timed curves, | 25 | it is, and it settles fast. Popups open and close on short timed curves, |
| 26 | slow enough to follow: a menu grows out of the pointer or its button as it | 26 | slow enough to follow: a menu grows out of the pointer or its button as it |
| 27 | fades in, a combo's field widens into its list, and a dialog swings up into | 27 | fades in, a combo's field widens into its list, and a dialog swings up into |
| 28 | place over a dimmed window, as Windows opens a window. | 28 | place over a dimmed window, as Windows opens a window. On GNOME and KDE, menus instead |
| 29 | look and move as the desktop's own (see [platforms](platforms.md)). | ||
| 29 | 30 | ||
| 30 | ## Why not native widgets | 31 | ## Why not native widgets |
| 31 | 32 |
corpus/structural-probe/README.md+3| ... | @@ -8,6 +8,9 @@ synthetic pages. Every page starts with a plain paragraph `Above`. | ... | @@ -8,6 +8,9 @@ synthetic pages. Every page starts with a plain paragraph `Above`. |
| 8 | page per case (`c1-bullet`, `c8-split`, ...). | 8 | page per case (`c1-bullet`, `c8-split`, ...). |
| 9 | - `tags.one`: a section holding one page with the nine Ctrl+1..9 default tags, | 9 | - `tags.one`: a section holding one page with the nine Ctrl+1..9 default tags, |
| 10 | closed and flushed by OneNote. | 10 | closed and flushed by OneNote. |
| 11 | - `tag-gallery.one`: one page with each of the 29 default tags of the Home tab's | ||
| 12 | Tags gallery applied from the gallery in order, one per paragraph (VM `tbar`, | ||
| 13 | 2026-09-28); two paragraphs also carry Call back. Copied while OneNote was open. | ||
| 11 | - `summaries/<case>-<step>.txt`: the page after each step, from COM | 14 | - `summaries/<case>-<step>.txt`: the page after each step, from COM |
| 12 | `GetPageContent`, one line per paragraph: | 15 | `GetPageContent`, one line per paragraph: |
| 13 | `L<level>[+indentN] [tags; [x]=completed] [list] qsN text`. | 16 | `L<level>[+indentN] [tags; [x]=completed] [list] qsN text`. |
corpus/structural-probe/tag-gallery.one created| Binary files /dev/null and b/corpus/structural-probe/tag-gallery.one differ | |||
crates/canvas/Cargo.toml+1| ... | @@ -34,6 +34,7 @@ name = "page-probe" | ... | @@ -34,6 +34,7 @@ name = "page-probe" |
| 34 | path = "src/bin/page_probe.rs" | 34 | path = "src/bin/page_probe.rs" |
| 35 | 35 | ||
| 36 | [dev-dependencies] | 36 | [dev-dependencies] |
| 37 | draw = { path = "../draw", features = ["wgpu"] } | ||
| 37 | accesskit_consumer = "0.39.0" | 38 | accesskit_consumer = "0.39.0" |
| 38 | pollster = "0.4" | 39 | pollster = "0.4" |
| 39 | wgpu.workspace = true | 40 | wgpu.workspace = true |
crates/canvas/src/editor.rs+221-27| ... | @@ -77,6 +77,14 @@ pub struct Selection { | ... | @@ -77,6 +77,14 @@ pub struct Selection { |
| 77 | pub affinities: [Affinity; 2], | 77 | pub affinities: [Affinity; 2], |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | /// What OneNote 2010's Ctrl+A selects beyond text: the focused outline as an object, then every | ||
| 81 | /// body outline on the page. | ||
| 82 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
| 83 | pub enum Whole { | ||
| 84 | Outline, | ||
| 85 | Page, | ||
| 86 | } | ||
| 87 | |||
| 80 | impl From<[TextPosition; 2]> for Selection { | 88 | impl From<[TextPosition; 2]> for Selection { |
| 81 | fn from(positions: [TextPosition; 2]) -> Self { | 89 | fn from(positions: [TextPosition; 2]) -> Self { |
| 82 | Self { | 90 | Self { |
| ... | @@ -105,6 +113,9 @@ pub struct CanvasEditor { | ... | @@ -105,6 +113,9 @@ pub struct CanvasEditor { |
| 105 | /// since, which `refresh` compares a changed stored page with; none once the editor | 113 | /// since, which `refresh` compares a changed stored page with; none once the editor |
| 106 | /// holds an edit that could not be stored. | 114 | /// holds an edit that could not be stored. |
| 107 | stored: Option<(Page, Vec<PageOp>)>, | 115 | stored: Option<(Page, Vec<PageOp>)>, |
| 116 | /// What Select All widened to, holding until the selection is moved or set, and while the | ||
| 117 | /// focused outline and its selection stay as they were then. | ||
| 118 | whole: Option<(Whole, ExGuid, Selection)>, | ||
| 108 | } | 119 | } |
| 109 | 120 | ||
| 110 | /// Imported page state the editable content does not carry. | 121 | /// Imported page state the editable content does not carry. |
| ... | @@ -433,6 +444,25 @@ impl TextOutline { | ... | @@ -433,6 +444,25 @@ impl TextOutline { |
| 433 | .join("\n") | 444 | .join("\n") |
| 434 | } | 445 | } |
| 435 | 446 | ||
| 447 | /// All the outline's shown text. | ||
| 448 | pub fn whole(&self) -> Selection { | ||
| 449 | let paragraph = self.source_index(self.shaped.paragraphs.len() - 1); | ||
| 450 | let last = self.document.paragraph(paragraph).unwrap(); | ||
| 451 | [ | ||
| 452 | TextPosition { | ||
| 453 | paragraph: 0, | ||
| 454 | offset: 0, | ||
| 455 | }, | ||
| 456 | TextPosition { | ||
| 457 | paragraph, | ||
| 458 | offset: last | ||
| 459 | .utf16_offset(last.text().len()) | ||
| 460 | .expect("paragraph text fits UTF-16 offsets"), | ||
| 461 | }, | ||
| 462 | ] | ||
| 463 | .into() | ||
| 464 | } | ||
| 465 | |||
| 436 | /// Highlight rectangles of `selection` in the outline, in its coordinates. | 466 | /// Highlight rectangles of `selection` in the outline, in its coordinates. |
| 437 | pub fn range_rects(&self, selection: Selection) -> Result<Vec<BoundingBox>, EditError> { | 467 | pub fn range_rects(&self, selection: Selection) -> Result<Vec<BoundingBox>, EditError> { |
| 438 | let [anchor, focus] = selection.positions; | 468 | let [anchor, focus] = selection.positions; |
| ... | @@ -712,6 +742,7 @@ struct Placement { | ... | @@ -712,6 +742,7 @@ struct Placement { |
| 712 | position: [Option<f32>; 2], | 742 | position: [Option<f32>; 2], |
| 713 | } | 743 | } |
| 714 | 744 | ||
| 745 | #[derive(Clone)] | ||
| 715 | enum RestoreFocus { | 746 | enum RestoreFocus { |
| 716 | Outline(ExGuid), | 747 | Outline(ExGuid), |
| 717 | Caret { | 748 | Caret { |
| ... | @@ -766,6 +797,8 @@ enum History { | ... | @@ -766,6 +797,8 @@ enum History { |
| 766 | selection: Selection, | 797 | selection: Selection, |
| 767 | focus: RestoreFocus, | 798 | focus: RestoreFocus, |
| 768 | }, | 799 | }, |
| 800 | /// One undo step of edits to OneNote's page selection, undone last first. | ||
| 801 | Group(Vec<History>), | ||
| 769 | } | 802 | } |
| 770 | 803 | ||
| 771 | impl CanvasEditor { | 804 | impl CanvasEditor { |
| ... | @@ -800,6 +833,7 @@ impl CanvasEditor { | ... | @@ -800,6 +833,7 @@ impl CanvasEditor { |
| 800 | pending: None, | 833 | pending: None, |
| 801 | ops: Ok(Vec::new()), | 834 | ops: Ok(Vec::new()), |
| 802 | stored: None, | 835 | stored: None, |
| 836 | whole: None, | ||
| 803 | }) | 837 | }) |
| 804 | } | 838 | } |
| 805 | 839 | ||
| ... | @@ -983,7 +1017,7 @@ impl CanvasEditor { | ... | @@ -983,7 +1017,7 @@ impl CanvasEditor { |
| 983 | } | 1017 | } |
| 984 | // Undoing an outline's creation focuses the outline focused before, if it is there. | 1018 | // Undoing an outline's creation focuses the outline focused before, if it is there. |
| 985 | let gone = |focus: &RestoreFocus| matches!(focus, RestoreFocus::Outline(id) if !fresh.outlines.iter().any(|o| o.id == *id)); | 1019 | let gone = |focus: &RestoreFocus| matches!(focus, RestoreFocus::Outline(id) if !fresh.outlines.iter().any(|o| o.id == *id)); |
| 986 | let reaches = |history: &History| match history { | 1020 | let reaches_one = |history: &History| match history { |
| 987 | History::Date(_) => date_changed, | 1021 | History::Date(_) => date_changed, |
| 988 | History::Image { .. } | History::Picture { .. } => objects_changed, | 1022 | History::Image { .. } | History::Picture { .. } => objects_changed, |
| 989 | History::Draft { outlines, .. } => changed.contains(&outlines[0].id), | 1023 | History::Draft { outlines, .. } => changed.contains(&outlines[0].id), |
| ... | @@ -1000,6 +1034,11 @@ impl CanvasEditor { | ... | @@ -1000,6 +1034,11 @@ impl CanvasEditor { |
| 1000 | History::Remove { outline, focus } => changed.contains(outline) || gone(focus), | 1034 | History::Remove { outline, focus } => changed.contains(outline) || gone(focus), |
| 1001 | History::Insert { outline, focus, .. } => changed.contains(&outline.id) || gone(focus), | 1035 | History::Insert { outline, focus, .. } => changed.contains(&outline.id) || gone(focus), |
| 1002 | History::Restore { source, focus, .. } => changed.contains(&source.id) || gone(focus), | 1036 | History::Restore { source, focus, .. } => changed.contains(&source.id) || gone(focus), |
| 1037 | History::Group(_) => unreachable!("groups do not nest"), | ||
| 1038 | }; | ||
| 1039 | let reaches = |history: &History| match history { | ||
| 1040 | History::Group(entries) => entries.iter().any(reaches_one), | ||
| 1041 | history => reaches_one(history), | ||
| 1003 | }; | 1042 | }; |
| 1004 | fresh.undo = std::mem::take(&mut self.undo) | 1043 | fresh.undo = std::mem::take(&mut self.undo) |
| 1005 | .into_iter() | 1044 | .into_iter() |
| ... | @@ -1347,6 +1386,7 @@ impl CanvasEditor { | ... | @@ -1347,6 +1386,7 @@ impl CanvasEditor { |
| 1347 | pending: None, | 1386 | pending: None, |
| 1348 | ops: Ok(Vec::new()), | 1387 | ops: Ok(Vec::new()), |
| 1349 | stored: None, | 1388 | stored: None, |
| 1389 | whole: None, | ||
| 1350 | }) | 1390 | }) |
| 1351 | } | 1391 | } |
| 1352 | 1392 | ||
| ... | @@ -2000,33 +2040,24 @@ impl CanvasEditor { | ... | @@ -2000,33 +2040,24 @@ impl CanvasEditor { |
| 2000 | } | 2040 | } |
| 2001 | self.finish_composition(); | 2041 | self.finish_composition(); |
| 2002 | self.preferred_x = None; | 2042 | self.preferred_x = None; |
| 2043 | self.whole = None; | ||
| 2003 | self.active_outline_mut().selection = selection; | 2044 | self.active_outline_mut().selection = selection; |
| 2004 | Ok(()) | 2045 | Ok(()) |
| 2005 | } | 2046 | } |
| 2006 | 2047 | ||
| 2007 | pub fn select_all(&mut self) -> Result<(), EditError> { | 2048 | pub fn select_all(&mut self) -> Result<(), EditError> { |
| 2008 | let paragraph = self | 2049 | self.select(self.active_outline().whole()) |
| 2009 | .active_outline() | ||
| 2010 | .source_index(self.active_outline().shaped.paragraphs.len() - 1); | ||
| 2011 | let last = self.active_outline().document.paragraph(paragraph).unwrap(); | ||
| 2012 | self.select( | ||
| 2013 | [ | ||
| 2014 | TextPosition { | ||
| 2015 | paragraph: 0, | ||
| 2016 | offset: 0, | ||
| 2017 | }, | ||
| 2018 | TextPosition { | ||
| 2019 | paragraph, | ||
| 2020 | offset: last.utf16_offset(last.text().len())?, | ||
| 2021 | }, | ||
| 2022 | ] | ||
| 2023 | .into(), | ||
| 2024 | ) | ||
| 2025 | } | 2050 | } |
| 2026 | 2051 | ||
| 2027 | /// Selects the smallest unit strictly holding the selection, as OneNote 2010's Ctrl+A widens | 2052 | /// Selects the smallest unit strictly holding the selection, as OneNote 2010's Ctrl+A widens |
| 2028 | /// it: the paragraph, its subtree, each enclosing table cell, row and table, then the outline. | 2053 | /// it: the paragraph, its subtree, each enclosing table cell, row and table, then a body |
| 2054 | /// outline as an object (the title's text instead), then every body outline on the page. | ||
| 2029 | pub fn widen_selection(&mut self) -> Result<(), EditError> { | 2055 | pub fn widen_selection(&mut self) -> Result<(), EditError> { |
| 2056 | match self.whole() { | ||
| 2057 | Some(Whole::Page) => return Ok(()), | ||
| 2058 | Some(Whole::Outline) => return self.select_page(), | ||
| 2059 | None => {} | ||
| 2060 | } | ||
| 2030 | let outline = self.active_outline(); | 2061 | let outline = self.active_outline(); |
| 2031 | let [anchor, focus] = outline.selection.positions; | 2062 | let [anchor, focus] = outline.selection.positions; |
| 2032 | let (start, end) = (anchor.min(focus), anchor.max(focus)); | 2063 | let (start, end) = (anchor.min(focus), anchor.max(focus)); |
| ... | @@ -2055,17 +2086,138 @@ impl CanvasEditor { | ... | @@ -2055,17 +2086,138 @@ impl CanvasEditor { |
| 2055 | .into_iter() | 2086 | .into_iter() |
| 2056 | .map(|range| (first(range.start), last_shown(range))), | 2087 | .map(|range| (first(range.start), last_shown(range))), |
| 2057 | ) | 2088 | ) |
| 2058 | .chain([( | 2089 | .chain(outline.title.then(|| { |
| 2059 | first(0), | 2090 | ( |
| 2060 | last_shown(0..outline.document.paragraphs().count()), | 2091 | first(0), |
| 2061 | )]); | 2092 | last_shown(0..outline.document.paragraphs().count()), |
| 2062 | match units | 2093 | ) |
| 2094 | })) | ||
| 2063 | .filter_map(|(unit_start, unit_end)| Some([unit_start, unit_end?])) | 2095 | .filter_map(|(unit_start, unit_end)| Some([unit_start, unit_end?])) |
| 2064 | .find(|&unit| unit[0] <= start && end <= unit[1] && unit != [start, end]) | 2096 | .find(|&unit| unit[0] <= start && end <= unit[1] && unit != [start, end]); |
| 2065 | { | 2097 | let (id, title) = (outline.id, outline.title); |
| 2098 | match units { | ||
| 2066 | Some(unit) => self.select(unit.into()), | 2099 | Some(unit) => self.select(unit.into()), |
| 2067 | None => Ok(()), | 2100 | None if title => self.select_page(), |
| 2101 | None => self.select_outline(id), | ||
| 2102 | } | ||
| 2103 | } | ||
| 2104 | |||
| 2105 | /// Selects outline `id` as an object, as a click on its handle does. | ||
| 2106 | pub fn select_outline(&mut self, id: ExGuid) -> Result<(), EditError> { | ||
| 2107 | self.focus_outline(id)?; | ||
| 2108 | let whole = self.active_outline().whole(); | ||
| 2109 | self.select(whole)?; | ||
| 2110 | self.whole = Some((Whole::Outline, id, whole)); | ||
| 2111 | Ok(()) | ||
| 2112 | } | ||
| 2113 | |||
| 2114 | /// What Select All has selected beyond text, if the selection is still what it chose. | ||
| 2115 | pub fn whole(&self) -> Option<Whole> { | ||
| 2116 | let (whole, id, selection) = self.whole?; | ||
| 2117 | let outline = self.active_outline(); | ||
| 2118 | (outline.id == id && outline.selection == selection && self.composition.is_none()) | ||
| 2119 | .then_some(whole) | ||
| 2120 | } | ||
| 2121 | |||
| 2122 | fn page_selected(&self) -> bool { | ||
| 2123 | self.whole() == Some(Whole::Page) | ||
| 2124 | } | ||
| 2125 | |||
| 2126 | /// Selects every body outline, focusing the last in page order as OneNote does: arrows | ||
| 2127 | /// leave the selection at its ends, and typing replaces its text. | ||
| 2128 | fn select_page(&mut self) -> Result<(), EditError> { | ||
| 2129 | let Some(last) = self.outlines.iter().rev().find(|outline| !outline.title) else { | ||
| 2130 | return Ok(()); | ||
| 2131 | }; | ||
| 2132 | let (id, whole) = (last.id, last.whole()); | ||
| 2133 | self.focus_outline(id)?; | ||
| 2134 | self.select(whole)?; | ||
| 2135 | self.whole = Some((Whole::Page, id, whole)); | ||
| 2136 | Ok(()) | ||
| 2137 | } | ||
| 2138 | |||
| 2139 | /// Runs `edit` as one undo step. | ||
| 2140 | fn grouped<T>( | ||
| 2141 | &mut self, | ||
| 2142 | edit: impl FnOnce(&mut Self) -> Result<T, EditorError>, | ||
| 2143 | ) -> Result<T, EditorError> { | ||
| 2144 | let depth = self.undo.len(); | ||
| 2145 | let result = edit(self); | ||
| 2146 | let entries = self.undo.split_off(depth); | ||
| 2147 | if !entries.is_empty() { | ||
| 2148 | self.undo.push(History::Group(entries)); | ||
| 2068 | } | 2149 | } |
| 2150 | result | ||
| 2151 | } | ||
| 2152 | |||
| 2153 | /// Removes the page selection's outlines, last first, but its last when `keep`, whose | ||
| 2154 | /// text stays selected to be replaced. Deleting the page selection leaves a caret where | ||
| 2155 | /// its first outline began, as OneNote 2010's does. | ||
| 2156 | fn remove_page(&mut self, engine: &mut TextEngine, keep: bool) -> Result<(), EditorError> { | ||
| 2157 | self.finish_composition(); | ||
| 2158 | self.whole = None; | ||
| 2159 | let mut body = (0..self.outlines.len()) | ||
| 2160 | .filter(|&index| !self.outlines[index].title) | ||
| 2161 | .collect::<Vec<_>>(); | ||
| 2162 | let Some(&first) = body.first() else { | ||
| 2163 | return Ok(()); | ||
| 2164 | }; | ||
| 2165 | let kept = if keep { body.pop() } else { None }; | ||
| 2166 | let caret = TextOutline::new( | ||
| 2167 | engine, | ||
| 2168 | TextDocument::new(vec![Paragraph::new(String::new(), Default::default())])?, | ||
| 2169 | DEFAULT_OUTLINE_WIDTH, | ||
| 2170 | self.outlines[first].origin(), | ||
| 2171 | )?; | ||
| 2172 | let index = self.outlines.len() - body.len(); | ||
| 2173 | let focus = RestoreFocus::Caret { | ||
| 2174 | source: Box::new(caret.snapshot()), | ||
| 2175 | selection: caret.selection, | ||
| 2176 | index, | ||
| 2177 | }; | ||
| 2178 | for &removed in body.iter().rev() { | ||
| 2179 | let outline = self.outlines.remove(removed); | ||
| 2180 | self.record(Ok(vec![PageOp::Delete { object: outline.id }])); | ||
| 2181 | self.undo.push(History::Insert { | ||
| 2182 | index: removed, | ||
| 2183 | outline: Box::new(outline), | ||
| 2184 | focus: focus.clone(), | ||
| 2185 | }); | ||
| 2186 | } | ||
| 2187 | self.redo.clear(); | ||
| 2188 | self.preferred_x = None; | ||
| 2189 | match kept { | ||
| 2190 | Some(kept) => { | ||
| 2191 | self.active = Focus::Outline(kept - body.len()); | ||
| 2192 | self.select_all()?; | ||
| 2193 | } | ||
| 2194 | None => { | ||
| 2195 | self.active = Focus::Caret { | ||
| 2196 | outline: Box::new(caret), | ||
| 2197 | index, | ||
| 2198 | } | ||
| 2199 | } | ||
| 2200 | } | ||
| 2201 | Ok(()) | ||
| 2202 | } | ||
| 2203 | |||
| 2204 | /// Moves every outline of the page selection by `delta`, as one undo step keeping it. | ||
| 2205 | pub fn move_page(&mut self, delta: [f32; 2]) -> Result<(), EditorError> { | ||
| 2206 | let moves = self | ||
| 2207 | .outlines | ||
| 2208 | .iter() | ||
| 2209 | .filter(|outline| !outline.title) | ||
| 2210 | .map(|outline| { | ||
| 2211 | let [x, y] = outline.origin(); | ||
| 2212 | (outline.id, [x + delta[0], y + delta[1]]) | ||
| 2213 | }) | ||
| 2214 | .collect::<Vec<_>>(); | ||
| 2215 | self.grouped(|editor| { | ||
| 2216 | for (id, position) in moves { | ||
| 2217 | editor.move_outline(id, position)?; | ||
| 2218 | } | ||
| 2219 | Ok(editor.select_page()?) | ||
| 2220 | }) | ||
| 2069 | } | 2221 | } |
| 2070 | 2222 | ||
| 2071 | pub fn select_at(&mut self, x: f32, y: f32, extend: bool) -> Result<(), EditError> { | 2223 | pub fn select_at(&mut self, x: f32, y: f32, extend: bool) -> Result<(), EditError> { |
| ... | @@ -2145,6 +2297,7 @@ impl CanvasEditor { | ... | @@ -2145,6 +2297,7 @@ impl CanvasEditor { |
| 2145 | extend: bool, | 2297 | extend: bool, |
| 2146 | ) -> Result<(), EditorError> { | 2298 | ) -> Result<(), EditorError> { |
| 2147 | self.finish_composition(); | 2299 | self.finish_composition(); |
| 2300 | self.whole = None; | ||
| 2148 | if !extend && movement == Movement::Right && self.leave_equation()? { | 2301 | if !extend && movement == Movement::Right && self.leave_equation()? { |
| 2149 | return Ok(()); | 2302 | return Ok(()); |
| 2150 | } | 2303 | } |
| ... | @@ -2614,6 +2767,12 @@ impl CanvasEditor { | ... | @@ -2614,6 +2767,12 @@ impl CanvasEditor { |
| 2614 | 2767 | ||
| 2615 | /// Types `text` at the selection; a space ends a typed URL, which becomes a link. | 2768 | /// Types `text` at the selection; a space ends a typed URL, which becomes a link. |
| 2616 | pub fn insert(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> { | 2769 | pub fn insert(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> { |
| 2770 | if self.page_selected() { | ||
| 2771 | return self.grouped(|editor| { | ||
| 2772 | editor.remove_page(engine, true)?; | ||
| 2773 | editor.insert(engine, text) | ||
| 2774 | }); | ||
| 2775 | } | ||
| 2617 | self.leave_link_code()?; | 2776 | self.leave_link_code()?; |
| 2618 | let start = self.active_outline().selection.positions[0] | 2777 | let start = self.active_outline().selection.positions[0] |
| 2619 | .min(self.active_outline().selection.positions[1]); | 2778 | .min(self.active_outline().selection.positions[1]); |
| ... | @@ -2640,6 +2799,12 @@ impl CanvasEditor { | ... | @@ -2640,6 +2799,12 @@ impl CanvasEditor { |
| 2640 | text: &str, | 2799 | text: &str, |
| 2641 | language: u32, | 2800 | language: u32, |
| 2642 | ) -> Result<(), EditorError> { | 2801 | ) -> Result<(), EditorError> { |
| 2802 | if self.page_selected() { | ||
| 2803 | return self.grouped(|editor| { | ||
| 2804 | editor.remove_page(engine, true)?; | ||
| 2805 | editor.paste(engine, text, language) | ||
| 2806 | }); | ||
| 2807 | } | ||
| 2643 | let lines = text | 2808 | let lines = text |
| 2644 | .split('\n') | 2809 | .split('\n') |
| 2645 | .map(|line| line.strip_suffix('\r').unwrap_or(line)) | 2810 | .map(|line| line.strip_suffix('\r').unwrap_or(line)) |
| ... | @@ -2888,6 +3053,10 @@ impl CanvasEditor { | ... | @@ -2888,6 +3053,10 @@ impl CanvasEditor { |
| 2888 | /// properties win; Delete at its end joins the paragraph below (`evidence/structural-edits/ | 3053 | /// properties win; Delete at its end joins the paragraph below (`evidence/structural-edits/ |
| 2889 | /// xml/c4-*`, `c5b-*`). Joins pass over a collapsed paragraph's hidden children. | 3054 | /// xml/c4-*`, `c5b-*`). Joins pass over a collapsed paragraph's hidden children. |
| 2890 | pub fn delete(&mut self, engine: &mut TextEngine, backward: bool) -> Result<bool, EditorError> { | 3055 | pub fn delete(&mut self, engine: &mut TextEngine, backward: bool) -> Result<bool, EditorError> { |
| 3056 | if self.page_selected() { | ||
| 3057 | self.grouped(|editor| editor.remove_page(engine, false))?; | ||
| 3058 | return Ok(true); | ||
| 3059 | } | ||
| 2891 | let selection = self.active_outline().selection; | 3060 | let selection = self.active_outline().selection; |
| 2892 | let [anchor, focus] = selection.positions; | 3061 | let [anchor, focus] = selection.positions; |
| 2893 | let mut range = anchor.min(focus)..anchor.max(focus); | 3062 | let mut range = anchor.min(focus)..anchor.max(focus); |
| ... | @@ -2975,6 +3144,9 @@ impl CanvasEditor { | ... | @@ -2975,6 +3144,9 @@ impl CanvasEditor { |
| 2975 | engine: &mut TextEngine, | 3144 | engine: &mut TextEngine, |
| 2976 | movement: Movement, | 3145 | movement: Movement, |
| 2977 | ) -> Result<bool, EditorError> { | 3146 | ) -> Result<bool, EditorError> { |
| 3147 | if self.page_selected() { | ||
| 3148 | return self.delete(engine, false); | ||
| 3149 | } | ||
| 2978 | self.finish_composition(); | 3150 | self.finish_composition(); |
| 2979 | let original = self.selection(); | 3151 | let original = self.selection(); |
| 2980 | let preferred_x = self.preferred_x; | 3152 | let preferred_x = self.preferred_x; |
| ... | @@ -3040,7 +3212,12 @@ impl CanvasEditor { | ... | @@ -3040,7 +3212,12 @@ impl CanvasEditor { |
| 3040 | }; | 3212 | }; |
| 3041 | match self.apply_history(engine, history) { | 3213 | match self.apply_history(engine, history) { |
| 3042 | Ok(inverse) => { | 3214 | Ok(inverse) => { |
| 3215 | let group = matches!(inverse, History::Group(_)); | ||
| 3043 | self.redo.push(inverse); | 3216 | self.redo.push(inverse); |
| 3217 | // Undoing an edit to the page selection selects the page again. | ||
| 3218 | if group { | ||
| 3219 | self.select_page()?; | ||
| 3220 | } | ||
| 3044 | Ok(true) | 3221 | Ok(true) |
| 3045 | } | 3222 | } |
| 3046 | Err((history, error)) => { | 3223 | Err((history, error)) => { |
| ... | @@ -3113,6 +3290,7 @@ impl CanvasEditor { | ... | @@ -3113,6 +3290,7 @@ impl CanvasEditor { |
| 3113 | History::Insert { index, .. } | History::Restore { index, .. } => { | 3290 | History::Insert { index, .. } | History::Restore { index, .. } => { |
| 3114 | *index <= self.outlines.len() | 3291 | *index <= self.outlines.len() |
| 3115 | } | 3292 | } |
| 3293 | History::Group(_) => true, | ||
| 3116 | }; | 3294 | }; |
| 3117 | if !valid { | 3295 | if !valid { |
| 3118 | return Err((history, EditError::InvalidRange.into())); | 3296 | return Err((history, EditError::InvalidRange.into())); |
| ... | @@ -3376,6 +3554,19 @@ impl CanvasEditor { | ... | @@ -3376,6 +3554,19 @@ impl CanvasEditor { |
| 3376 | self.active = Focus::Outline(index); | 3554 | self.active = Focus::Outline(index); |
| 3377 | History::Remove { outline: id, focus } | 3555 | History::Remove { outline: id, focus } |
| 3378 | } | 3556 | } |
| 3557 | History::Group(mut entries) => { | ||
| 3558 | let mut inverses = Vec::with_capacity(entries.len()); | ||
| 3559 | while let Some(entry) = entries.pop() { | ||
| 3560 | match self.apply_history(engine, entry) { | ||
| 3561 | Ok(inverse) => inverses.push(inverse), | ||
| 3562 | Err((entry, error)) => { | ||
| 3563 | entries.push(entry); | ||
| 3564 | return Err((History::Group(entries), error)); | ||
| 3565 | } | ||
| 3566 | } | ||
| 3567 | } | ||
| 3568 | History::Group(inverses) | ||
| 3569 | } | ||
| 3379 | }; | 3570 | }; |
| 3380 | self.preferred_x = None; | 3571 | self.preferred_x = None; |
| 3381 | Ok(inverse) | 3572 | Ok(inverse) |
| ... | @@ -3396,6 +3587,9 @@ impl CanvasEditor { | ... | @@ -3396,6 +3587,9 @@ impl CanvasEditor { |
| 3396 | if selected.start > selected.end { | 3587 | if selected.start > selected.end { |
| 3397 | return Err(EditError::InvalidRange.into()); | 3588 | return Err(EditError::InvalidRange.into()); |
| 3398 | } | 3589 | } |
| 3590 | if self.page_selected() { | ||
| 3591 | self.grouped(|editor| editor.remove_page(engine, true))?; | ||
| 3592 | } | ||
| 3399 | let range = self.marked_range().unwrap_or_else(|| { | 3593 | let range = self.marked_range().unwrap_or_else(|| { |
| 3400 | self.active_outline().selection.positions[0] | 3594 | self.active_outline().selection.positions[0] |
| 3401 | .min(self.active_outline().selection.positions[1]) | 3595 | .min(self.active_outline().selection.positions[1]) |
crates/canvas/src/editor/format.rs+247-35| ... | @@ -1,13 +1,14 @@ | ... | @@ -1,13 +1,14 @@ |
| 1 | //! OneNote's Home-ribbon commands: character formatting, lists, alignment and note tags. | 1 | //! OneNote's Home-ribbon commands: character formatting, lists, alignment and note tags. |
| 2 | 2 | ||
| 3 | use super::*; | 3 | use super::*; |
| 4 | use crate::layout::{DEFAULT_FONT, DEFAULT_FONT_SIZE}; | ||
| 4 | use onestore::document::{Format, Kind, Tag}; | 5 | use onestore::document::{Format, Kind, Tag}; |
| 5 | use onestore::page::ParagraphContent; | 6 | use onestore::page::ParagraphContent; |
| 6 | use onestore::page::text::new_id; | 7 | use onestore::page::text::new_id; |
| 7 | 8 | ||
| 8 | /// The COLORREF OneNote stores for automatic colour, which overrides a style's colour. | 9 | /// The COLORREF OneNote stores for automatic colour, which overrides a style's colour. |
| 9 | const AUTOMATIC: u32 = 0xff00_0000; | 10 | const AUTOMATIC: u32 = 0xff00_0000; |
| 10 | /// The one checkable note tag shape the page draws. | 11 | /// To Do's check box. |
| 11 | const CHECKBOX: u16 = 3; | 12 | const CHECKBOX: u16 = 3; |
| 12 | 13 | ||
| 13 | /// A toolbar command on the selection, or with only a caret, on the text typed there next. | 14 | /// A toolbar command on the selection, or with only a caret, on the text typed there next. |
| ... | @@ -36,6 +37,9 @@ pub enum Formatting { | ... | @@ -36,6 +37,9 @@ pub enum Formatting { |
| 36 | RemoveTags, | 37 | RemoveTags, |
| 37 | /// Checks the selected paragraphs' check boxes, or clears them when all are checked. | 38 | /// Checks the selected paragraphs' check boxes, or clears them when all are checked. |
| 38 | Check, | 39 | Check, |
| 40 | /// Format Painter: gives the selection the character formatting and alignment of text | ||
| 41 | /// picked up with [`CanvasEditor::painted_format`], as OneNote's does. | ||
| 42 | Paint(Format), | ||
| 39 | } | 43 | } |
| 40 | 44 | ||
| 41 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | 45 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| ... | @@ -157,8 +161,9 @@ impl ListStyle { | ... | @@ -157,8 +161,9 @@ impl ListStyle { |
| 157 | } | 161 | } |
| 158 | } | 162 | } |
| 159 | 163 | ||
| 160 | /// OneNote 2010's default tags, Ctrl+1 to Ctrl+9, as it stores their definitions | 164 | /// OneNote 2010's default tags, the first nine on Ctrl+1 to Ctrl+9, as it stores their |
| 161 | /// (`corpus/structural-probe/tags.one`); declared in that order, their action types. | 165 | /// definitions (`corpus/structural-probe/tag-gallery.one`); declared in gallery order, their |
| 166 | /// action types. | ||
| 162 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | 167 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 163 | pub enum NoteTag { | 168 | pub enum NoteTag { |
| 164 | ToDo, | 169 | ToDo, |
| ... | @@ -170,6 +175,26 @@ pub enum NoteTag { | ... | @@ -170,6 +175,26 @@ pub enum NoteTag { |
| 170 | Contact, | 175 | Contact, |
| 171 | Address, | 176 | Address, |
| 172 | PhoneNumber, | 177 | PhoneNumber, |
| 178 | WebSiteToVisit, | ||
| 179 | Idea, | ||
| 180 | Password, | ||
| 181 | Critical, | ||
| 182 | ProjectA, | ||
| 183 | ProjectB, | ||
| 184 | MovieToSee, | ||
| 185 | BookToRead, | ||
| 186 | MusicToListenTo, | ||
| 187 | SourceForArticle, | ||
| 188 | RememberForBlog, | ||
| 189 | DiscussWithPersonA, | ||
| 190 | DiscussWithPersonB, | ||
| 191 | DiscussWithManager, | ||
| 192 | SendInEmail, | ||
| 193 | ScheduleMeeting, | ||
| 194 | CallBack, | ||
| 195 | ToDoPriority1, | ||
| 196 | ToDoPriority2, | ||
| 197 | ClientRequest, | ||
| 173 | } | 198 | } |
| 174 | 199 | ||
| 175 | /// What the selection shows on the toolbar. | 200 | /// What the selection shows on the toolbar. |
| ... | @@ -177,7 +202,7 @@ pub enum NoteTag { | ... | @@ -177,7 +202,7 @@ pub enum NoteTag { |
| 177 | pub struct FormatState { | 202 | pub struct FormatState { |
| 178 | /// Attributes every selected character has. | 203 | /// Attributes every selected character has. |
| 179 | pub toggles: Vec<Toggle>, | 204 | pub toggles: Vec<Toggle>, |
| 180 | /// The font and size every selected character shares. | 205 | /// The font and size every selected character is laid out in, where they share one. |
| 181 | pub font: Option<String>, | 206 | pub font: Option<String>, |
| 182 | pub font_size: Option<f32>, | 207 | pub font_size: Option<f32>, |
| 183 | /// The alignment every selected paragraph shares. | 208 | /// The alignment every selected paragraph shares. |
| ... | @@ -225,7 +250,7 @@ impl Toggle { | ... | @@ -225,7 +250,7 @@ impl Toggle { |
| 225 | } | 250 | } |
| 226 | 251 | ||
| 227 | impl NoteTag { | 252 | impl NoteTag { |
| 228 | pub(crate) const ALL: [Self; 9] = [ | 253 | pub const ALL: [Self; 29] = [ |
| 229 | Self::ToDo, | 254 | Self::ToDo, |
| 230 | Self::Important, | 255 | Self::Important, |
| 231 | Self::Question, | 256 | Self::Question, |
| ... | @@ -235,6 +260,26 @@ impl NoteTag { | ... | @@ -235,6 +260,26 @@ impl NoteTag { |
| 235 | Self::Contact, | 260 | Self::Contact, |
| 236 | Self::Address, | 261 | Self::Address, |
| 237 | Self::PhoneNumber, | 262 | Self::PhoneNumber, |
| 263 | Self::WebSiteToVisit, | ||
| 264 | Self::Idea, | ||
| 265 | Self::Password, | ||
| 266 | Self::Critical, | ||
| 267 | Self::ProjectA, | ||
| 268 | Self::ProjectB, | ||
| 269 | Self::MovieToSee, | ||
| 270 | Self::BookToRead, | ||
| 271 | Self::MusicToListenTo, | ||
| 272 | Self::SourceForArticle, | ||
| 273 | Self::RememberForBlog, | ||
| 274 | Self::DiscussWithPersonA, | ||
| 275 | Self::DiscussWithPersonB, | ||
| 276 | Self::DiscussWithManager, | ||
| 277 | Self::SendInEmail, | ||
| 278 | Self::ScheduleMeeting, | ||
| 279 | Self::CallBack, | ||
| 280 | Self::ToDoPriority1, | ||
| 281 | Self::ToDoPriority2, | ||
| 282 | Self::ClientRequest, | ||
| 238 | ]; | 283 | ]; |
| 239 | 284 | ||
| 240 | /// The tag's name, as OneNote stores and lists it. | 285 | /// The tag's name, as OneNote stores and lists it. |
| ... | @@ -249,26 +294,71 @@ impl NoteTag { | ... | @@ -249,26 +294,71 @@ impl NoteTag { |
| 249 | Self::Contact => "Contact", | 294 | Self::Contact => "Contact", |
| 250 | Self::Address => "Address", | 295 | Self::Address => "Address", |
| 251 | Self::PhoneNumber => "Phone number", | 296 | Self::PhoneNumber => "Phone number", |
| 297 | Self::WebSiteToVisit => "Web site to visit", | ||
| 298 | Self::Idea => "Idea", | ||
| 299 | Self::Password => "Password", | ||
| 300 | Self::Critical => "Critical", | ||
| 301 | Self::ProjectA => "Project A", | ||
| 302 | Self::ProjectB => "Project B", | ||
| 303 | Self::MovieToSee => "Movie to see", | ||
| 304 | Self::BookToRead => "Book to read", | ||
| 305 | Self::MusicToListenTo => "Music to listen to", | ||
| 306 | Self::SourceForArticle => "Source for article", | ||
| 307 | Self::RememberForBlog => "Remember for blog", | ||
| 308 | Self::DiscussWithPersonA => "Discuss with <Person A>", | ||
| 309 | Self::DiscussWithPersonB => "Discuss with <Person B>", | ||
| 310 | Self::DiscussWithManager => "Discuss with manager", | ||
| 311 | Self::SendInEmail => "Send in e-mail", | ||
| 312 | Self::ScheduleMeeting => "Schedule meeting", | ||
| 313 | Self::CallBack => "Call back", | ||
| 314 | Self::ToDoPriority1 => "To Do priority 1", | ||
| 315 | Self::ToDoPriority2 => "To Do priority 2", | ||
| 316 | Self::ClientRequest => "Client request", | ||
| 317 | } | ||
| 318 | } | ||
| 319 | |||
| 320 | /// The symbol OneNote draws for the tag; 0 draws none. | ||
| 321 | pub const fn shape(self) -> u16 { | ||
| 322 | match self { | ||
| 323 | Self::ToDo => CHECKBOX, | ||
| 324 | Self::Important => 13, | ||
| 325 | Self::Question => 15, | ||
| 326 | Self::RememberForLater | Self::Definition => 0, | ||
| 327 | Self::Highlight => 136, | ||
| 328 | Self::Contact => 118, | ||
| 329 | Self::Address => 23, | ||
| 330 | Self::PhoneNumber => 18, | ||
| 331 | Self::WebSiteToVisit | Self::SourceForArticle => 125, | ||
| 332 | Self::Idea => 21, | ||
| 333 | Self::Password => 131, | ||
| 334 | Self::Critical => 17, | ||
| 335 | Self::ProjectA => 100, | ||
| 336 | Self::ProjectB => 101, | ||
| 337 | Self::MovieToSee => 122, | ||
| 338 | Self::BookToRead => 132, | ||
| 339 | Self::MusicToListenTo => 121, | ||
| 340 | Self::RememberForBlog => 24, | ||
| 341 | Self::DiscussWithPersonA | Self::DiscussWithPersonB => 94, | ||
| 342 | Self::DiscussWithManager => 95, | ||
| 343 | Self::SendInEmail => 106, | ||
| 344 | Self::ScheduleMeeting | Self::CallBack => 12, | ||
| 345 | Self::ToDoPriority1 => 28, | ||
| 346 | Self::ToDoPriority2 => 71, | ||
| 347 | Self::ClientRequest => 8, | ||
| 252 | } | 348 | } |
| 253 | } | 349 | } |
| 254 | 350 | ||
| 255 | pub(super) fn definition(self) -> Definition { | 351 | pub(super) fn definition(self) -> Definition { |
| 256 | let (shape, highlight) = match self { | 352 | let highlight = match self { |
| 257 | Self::ToDo => (CHECKBOX, None), | 353 | Self::RememberForLater => Some(0x0000_ffff), |
| 258 | Self::Important => (13, None), | 354 | Self::Definition => Some(0x0000_ff00), |
| 259 | Self::Question => (15, None), | 355 | _ => None, |
| 260 | Self::RememberForLater => (0, Some(0x0000_ffff)), | ||
| 261 | Self::Definition => (0, Some(0x0000_ff00)), | ||
| 262 | Self::Highlight => (136, None), | ||
| 263 | Self::Contact => (118, None), | ||
| 264 | Self::Address => (23, None), | ||
| 265 | Self::PhoneNumber => (18, None), | ||
| 266 | }; | 356 | }; |
| 267 | Definition { | 357 | Definition { |
| 268 | kind: Kind::TagDefinition { | 358 | kind: Kind::TagDefinition { |
| 269 | label: Some(self.label().into()), | 359 | label: Some(self.label().into()), |
| 270 | action_type: Some(self as u16), | 360 | action_type: Some(self as u16), |
| 271 | shape: Some(shape), | 361 | shape: Some(self.shape()), |
| 272 | // The highlighting tags also set black text. | 362 | // The highlighting tags also set black text. |
| 273 | color: highlight.map(|_| 0), | 363 | color: highlight.map(|_| 0), |
| 274 | highlight, | 364 | highlight, |
| ... | @@ -278,6 +368,14 @@ impl NoteTag { | ... | @@ -278,6 +368,14 @@ impl NoteTag { |
| 278 | } | 368 | } |
| 279 | } | 369 | } |
| 280 | 370 | ||
| 371 | /// Whether tag symbol `shape` is a check box, which OneNote stores unchecked when applied. | ||
| 372 | fn checkable(shape: Option<u16>) -> bool { | ||
| 373 | matches!( | ||
| 374 | shape.and_then(|shape| crate::outline::TagIcon::of(shape, false)), | ||
| 375 | Some(crate::outline::TagIcon::CheckBox { .. }) | ||
| 376 | ) | ||
| 377 | } | ||
| 378 | |||
| 281 | /// Where a selection starts and ends, as text leaf identity and byte offset. | 379 | /// Where a selection starts and ends, as text leaf identity and byte offset. |
| 282 | type Ends = [(ExGuid, usize); 2]; | 380 | type Ends = [(ExGuid, usize); 2]; |
| 283 | 381 | ||
| ... | @@ -494,16 +592,37 @@ impl CanvasEditor { | ... | @@ -494,16 +592,37 @@ impl CanvasEditor { |
| 494 | Some(&self.definitions.get(tag.definition.as_ref()?)?.kind) | 592 | Some(&self.definitions.get(tag.definition.as_ref()?)?.kind) |
| 495 | } | 593 | } |
| 496 | 594 | ||
| 497 | pub fn format_state(&self) -> Result<FormatState, EditorError> { | 595 | /// What Format Painter picks up: the first selected character's formatting, or with only |
| 596 | /// a caret, what would be typed there. | ||
| 597 | pub fn painted_format(&self) -> Result<Format, EditorError> { | ||
| 498 | let outline = self.active_outline(); | 598 | let outline = self.active_outline(); |
| 499 | let (container, range, ends) = selected(&outline.document, outline.selection)?; | 599 | let (container, range, ends) = selected(&outline.document, outline.selection)?; |
| 600 | let [anchor, focus] = outline.selection.positions; | ||
| 601 | let nodes = &outline.document.container(container)?[range]; | ||
| 602 | match character_formats(covered(nodes, ends)).first() { | ||
| 603 | Some(format) if ends[0] != ends[1] => Ok((*format).clone()), | ||
| 604 | _ => Ok(self.typing_format(anchor.min(focus))?), | ||
| 605 | } | ||
| 606 | } | ||
| 607 | |||
| 608 | pub fn format_state(&self) -> Result<FormatState, EditorError> { | ||
| 609 | let outline = self.active_outline(); | ||
| 610 | self.format_state_of(outline, outline.selection) | ||
| 611 | } | ||
| 612 | |||
| 613 | fn format_state_of( | ||
| 614 | &self, | ||
| 615 | outline: &TextOutline, | ||
| 616 | selection: Selection, | ||
| 617 | ) -> Result<FormatState, EditorError> { | ||
| 618 | let (container, range, ends) = selected(&outline.document, selection)?; | ||
| 500 | let nodes = &outline.document.container(container)?[range]; | 619 | let nodes = &outline.document.container(container)?[range]; |
| 501 | let paragraphs = covered(nodes, ends) | 620 | let paragraphs = covered(nodes, ends) |
| 502 | .map(|(node, _)| node) | 621 | .map(|(node, _)| node) |
| 503 | .collect::<Vec<_>>(); | 622 | .collect::<Vec<_>>(); |
| 504 | let caret; | 623 | let caret; |
| 505 | let formats = if ends[0] == ends[1] { | 624 | let formats = if ends[0] == ends[1] { |
| 506 | let [anchor, focus] = outline.selection.positions; | 625 | let [anchor, focus] = selection.positions; |
| 507 | caret = self.typing_format(anchor.min(focus))?; | 626 | caret = self.typing_format(anchor.min(focus))?; |
| 508 | vec![&caret] | 627 | vec![&caret] |
| 509 | } else { | 628 | } else { |
| ... | @@ -523,8 +642,16 @@ impl CanvasEditor { | ... | @@ -523,8 +642,16 @@ impl CanvasEditor { |
| 523 | .all(|format| toggle.get(format) == Some(true)) | 642 | .all(|format| toggle.get(format) == Some(true)) |
| 524 | }) | 643 | }) |
| 525 | .collect(), | 644 | .collect(), |
| 526 | font: common(formats.iter().map(|format| format.font.clone())), | 645 | font: common( |
| 527 | font_size: common(formats.iter().map(|format| format.font_size)), | 646 | formats |
| 647 | .iter() | ||
| 648 | .map(|format| Some(format.font.as_deref().unwrap_or(DEFAULT_FONT).to_owned())), | ||
| 649 | ), | ||
| 650 | font_size: common( | ||
| 651 | formats | ||
| 652 | .iter() | ||
| 653 | .map(|format| Some(format.font_size.unwrap_or(DEFAULT_FONT_SIZE))), | ||
| 654 | ), | ||
| 528 | alignment: common(paragraphs.iter().map(|node| { | 655 | alignment: common(paragraphs.iter().map(|node| { |
| 529 | Some( | 656 | Some( |
| 530 | match node.text().unwrap().text.spans()[0].format.alignment { | 657 | match node.text().unwrap().text.spans()[0].format.alignment { |
| ... | @@ -555,6 +682,9 @@ impl CanvasEditor { | ... | @@ -555,6 +682,9 @@ impl CanvasEditor { |
| 555 | engine: &mut TextEngine, | 682 | engine: &mut TextEngine, |
| 556 | command: Formatting, | 683 | command: Formatting, |
| 557 | ) -> Result<(), EditorError> { | 684 | ) -> Result<(), EditorError> { |
| 685 | if self.page_selected() { | ||
| 686 | return self.format_page(engine, command); | ||
| 687 | } | ||
| 558 | let outline = self.active_outline(); | 688 | let outline = self.active_outline(); |
| 559 | let (id, title, selection) = (outline.id, outline.title, outline.selection); | 689 | let (id, title, selection) = (outline.id, outline.title, outline.selection); |
| 560 | let (container, mut range, ends) = selected(&outline.document, selection)?; | 690 | let (container, mut range, ends) = selected(&outline.document, selection)?; |
| ... | @@ -696,7 +826,7 @@ impl CanvasEditor { | ... | @@ -696,7 +826,7 @@ impl CanvasEditor { |
| 696 | }); | 826 | }); |
| 697 | } | 827 | } |
| 698 | if !remove { | 828 | if !remove { |
| 699 | let checkable = shape == Some(CHECKBOX); | 829 | let checkable = checkable(shape); |
| 700 | // Stored newest first. | 830 | // Stored newest first. |
| 701 | text.tags.insert( | 831 | text.tags.insert( |
| 702 | 0, | 832 | 0, |
| ... | @@ -727,6 +857,7 @@ impl CanvasEditor { | ... | @@ -727,6 +857,7 @@ impl CanvasEditor { |
| 727 | | Formatting::FontSize(_) | 857 | | Formatting::FontSize(_) |
| 728 | | Formatting::Color(_) | 858 | | Formatting::Color(_) |
| 729 | | Formatting::Highlight(_) | 859 | | Formatting::Highlight(_) |
| 860 | | Formatting::Paint(_) | ||
| 730 | | Formatting::Clear => { | 861 | | Formatting::Clear => { |
| 731 | let caret = (ends[0] == ends[1]) | 862 | let caret = (ends[0] == ends[1]) |
| 732 | .then(|| { | 863 | .then(|| { |
| ... | @@ -766,6 +897,15 @@ impl CanvasEditor { | ... | @@ -766,6 +897,15 @@ impl CanvasEditor { |
| 766 | Formatting::Highlight(color) => { | 897 | Formatting::Highlight(color) => { |
| 767 | format.highlight = color.or(base.highlight.map(|_| AUTOMATIC)); | 898 | format.highlight = color.or(base.highlight.map(|_| AUTOMATIC)); |
| 768 | } | 899 | } |
| 900 | Formatting::Paint(painted) => { | ||
| 901 | for toggle in Toggle::ALL { | ||
| 902 | *toggle.slot(format) = toggle.get(painted).or(toggle.get(base)); | ||
| 903 | } | ||
| 904 | format.font = painted.font.clone().or_else(|| base.font.clone()); | ||
| 905 | format.font_size = painted.font_size.or(base.font_size); | ||
| 906 | format.color = painted.color.or(base.color); | ||
| 907 | format.highlight = painted.highlight.or(base.highlight); | ||
| 908 | } | ||
| 769 | _ => { | 909 | _ => { |
| 770 | for toggle in Toggle::ALL { | 910 | for toggle in Toggle::ALL { |
| 771 | *toggle.slot(format) = toggle.get(base); | 911 | *toggle.slot(format) = toggle.get(base); |
| ... | @@ -787,6 +927,13 @@ impl CanvasEditor { | ... | @@ -787,6 +927,13 @@ impl CanvasEditor { |
| 787 | let base = &bases[&node.id]; | 927 | let base = &bases[&node.id]; |
| 788 | let text = &mut node.text_mut().unwrap().text; | 928 | let text = &mut node.text_mut().unwrap().text; |
| 789 | *text = restyle(text, range, |format| change(base, format)); | 929 | *text = restyle(text, range, |format| change(base, format)); |
| 930 | if let Formatting::Paint(painted) = &command | ||
| 931 | && !title | ||
| 932 | { | ||
| 933 | *text = restyle(text, 0..text.text().len(), |format| { | ||
| 934 | format.alignment = painted.alignment; | ||
| 935 | }); | ||
| 936 | } | ||
| 790 | } | 937 | } |
| 791 | }); | 938 | }); |
| 792 | } | 939 | } |
| ... | @@ -808,6 +955,40 @@ impl CanvasEditor { | ... | @@ -808,6 +955,40 @@ impl CanvasEditor { |
| 808 | } | 955 | } |
| 809 | 956 | ||
| 810 | impl CanvasEditor { | 957 | impl CanvasEditor { |
| 958 | /// A toolbar command on OneNote 2010's page selection: each outline takes it as if selected | ||
| 959 | /// alone, but a toggle turns on everywhere unless every outline already has it. | ||
| 960 | fn format_page( | ||
| 961 | &mut self, | ||
| 962 | engine: &mut TextEngine, | ||
| 963 | command: Formatting, | ||
| 964 | ) -> Result<(), EditorError> { | ||
| 965 | self.whole = None; | ||
| 966 | let has = |state: FormatState| match &command { | ||
| 967 | Formatting::Toggle(toggle) => state.toggles.contains(toggle), | ||
| 968 | Formatting::Bullets => state.bullets, | ||
| 969 | Formatting::Numbering => state.numbering, | ||
| 970 | Formatting::Tag(tag) => state.tags.contains(tag), | ||
| 971 | _ => false, | ||
| 972 | }; | ||
| 973 | let mut outlines = Vec::new(); | ||
| 974 | for outline in self.outlines.iter().filter(|outline| !outline.title) { | ||
| 975 | let state = self.format_state_of(outline, outline.whole())?; | ||
| 976 | outlines.push((outline.id, has(state))); | ||
| 977 | } | ||
| 978 | let everywhere = outlines.iter().all(|(_, has)| *has); | ||
| 979 | self.grouped(|editor| { | ||
| 980 | for (id, has) in outlines { | ||
| 981 | if has && !everywhere { | ||
| 982 | continue; | ||
| 983 | } | ||
| 984 | editor.focus_outline(id)?; | ||
| 985 | editor.select_all()?; | ||
| 986 | editor.format(engine, command.clone())?; | ||
| 987 | } | ||
| 988 | Ok(editor.select_page()?) | ||
| 989 | }) | ||
| 990 | } | ||
| 991 | |||
| 811 | /// A click on a check box tag: checks the check boxes of paragraph `id` in outline | 992 | /// A click on a check box tag: checks the check boxes of paragraph `id` in outline |
| 812 | /// `outline`, or clears them once all are checked, as one undo step that keeps the | 993 | /// `outline`, or clears them once all are checked, as one undo step that keeps the |
| 813 | /// selection. | 994 | /// selection. |
| ... | @@ -857,10 +1038,7 @@ impl CanvasEditor { | ... | @@ -857,10 +1038,7 @@ impl CanvasEditor { |
| 857 | let checkable = |tag: &Tag| { | 1038 | let checkable = |tag: &Tag| { |
| 858 | matches!( | 1039 | matches!( |
| 859 | self.tag_kind(tag), | 1040 | self.tag_kind(tag), |
| 860 | Some(Kind::TagDefinition { | 1041 | Some(Kind::TagDefinition { shape, .. }) if checkable(*shape) |
| 861 | shape: Some(CHECKBOX), | ||
| 862 | .. | ||
| 863 | }) | ||
| 864 | ) | 1042 | ) |
| 865 | }; | 1043 | }; |
| 866 | let checked = self::covered(replacement, ends) | 1044 | let checked = self::covered(replacement, ends) |
| ... | @@ -920,7 +1098,7 @@ pub(super) fn nested_list(definition: &Definition, deeper: bool) -> Option<Defin | ... | @@ -920,7 +1098,7 @@ pub(super) fn nested_list(definition: &Definition, deeper: bool) -> Option<Defin |
| 920 | /// and its Ctrl+. bullet store it (`corpus/paragraph-edit/reconciliation/keyboard`); a number | 1098 | /// and its Ctrl+. bullet store it (`corpus/paragraph-edit/reconciliation/keyboard`); a number |
| 921 | /// also takes the text's font and language (`corpus/outline-edit/tree`). | 1099 | /// also takes the text's font and language (`corpus/outline-edit/tree`). |
| 922 | pub(super) fn list_definition(style: ListStyle, format: &Format) -> Definition { | 1100 | pub(super) fn list_definition(style: ListStyle, format: &Format) -> Definition { |
| 923 | let font_size = Some(format.font_size.unwrap_or(11.0)); | 1101 | let font_size = Some(format.font_size.unwrap_or(DEFAULT_FONT_SIZE)); |
| 924 | match style { | 1102 | match style { |
| 925 | ListStyle::Number(index) => Definition { | 1103 | ListStyle::Number(index) => Definition { |
| 926 | kind: Kind::List { | 1104 | kind: Kind::List { |
| ... | @@ -1765,9 +1943,9 @@ mod tests { | ... | @@ -1765,9 +1943,9 @@ mod tests { |
| 1765 | } | 1943 | } |
| 1766 | 1944 | ||
| 1767 | #[test] | 1945 | #[test] |
| 1768 | fn the_nine_default_tags_are_onenotes_stored_definitions_and_all_draw() { | 1946 | fn the_default_tags_are_onenotes_stored_definitions_and_all_draw() { |
| 1769 | use onestore::{RevisionIndex, Store, document::Document}; | 1947 | use onestore::{RevisionIndex, Store, document::Document}; |
| 1770 | let bytes = include_bytes!("../../../../corpus/structural-probe/tags.one"); | 1948 | let bytes = include_bytes!("../../../../corpus/structural-probe/tag-gallery.one"); |
| 1771 | let store = Store::parse(bytes).unwrap(); | 1949 | let store = Store::parse(bytes).unwrap(); |
| 1772 | let index = RevisionIndex::parse(&store).unwrap(); | 1950 | let index = RevisionIndex::parse(&store).unwrap(); |
| 1773 | let document = Document::parse(&index).unwrap(); | 1951 | let document = Document::parse(&index).unwrap(); |
| ... | @@ -1789,12 +1967,14 @@ mod tests { | ... | @@ -1789,12 +1967,14 @@ mod tests { |
| 1789 | assert_eq!(editor.format_state().unwrap().tags, NoteTag::ALL); | 1967 | assert_eq!(editor.format_state().unwrap().tags, NoteTag::ALL); |
| 1790 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; | 1968 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; |
| 1791 | use crate::outline::TagIcon; | 1969 | use crate::outline::TagIcon; |
| 1970 | let icons = paragraph | ||
| 1971 | .tags | ||
| 1972 | .iter() | ||
| 1973 | .map(|tag| tag.icon) | ||
| 1974 | .collect::<Vec<_>>(); | ||
| 1975 | assert_eq!(icons.len(), NoteTag::ALL.len() - 2); | ||
| 1792 | assert_eq!( | 1976 | assert_eq!( |
| 1793 | paragraph | 1977 | icons[..7], |
| 1794 | .tags | ||
| 1795 | .iter() | ||
| 1796 | .map(|tag| tag.icon) | ||
| 1797 | .collect::<Vec<_>>(), | ||
| 1798 | [ | 1978 | [ |
| 1799 | TagIcon::CheckBox { checked: false }, | 1979 | TagIcon::CheckBox { checked: false }, |
| 1800 | TagIcon::Star, | 1980 | TagIcon::Star, |
| ... | @@ -1805,6 +1985,13 @@ mod tests { | ... | @@ -1805,6 +1985,13 @@ mod tests { |
| 1805 | TagIcon::Phone, | 1985 | TagIcon::Phone, |
| 1806 | ] | 1986 | ] |
| 1807 | ); | 1987 | ); |
| 1988 | // As OneNote stores them unchecked: To Do, Discuss with, Schedule meeting, Call back, | ||
| 1989 | // the priorities and Client request. | ||
| 1990 | let checkable = NoteTag::ALL | ||
| 1991 | .into_iter() | ||
| 1992 | .filter(|tag| checkable(Some(tag.shape()))) | ||
| 1993 | .count(); | ||
| 1994 | assert_eq!(checkable, 9); | ||
| 1808 | // Remember for later and Definition draw no symbol; the newer one's green marks the text. | 1995 | // Remember for later and Definition draw no symbol; the newer one's green marks the text. |
| 1809 | assert!( | 1996 | assert!( |
| 1810 | paragraph | 1997 | paragraph |
| ... | @@ -1885,8 +2072,8 @@ mod tests { | ... | @@ -1885,8 +2072,8 @@ mod tests { |
| 1885 | let settled = |mut page: Page| { | 2072 | let settled = |mut page: Page| { |
| 1886 | for object in &mut page.objects { | 2073 | for object in &mut page.objects { |
| 1887 | if let PageObject::Outline(outline) = object { | 2074 | if let PageObject::Outline(outline) = object { |
| 1888 | for paragraph in &mut outline.paragraphs { | 2075 | for text in outline.paragraphs.iter_mut().filter_map(|p| p.text_mut()) { |
| 1889 | for tag in &mut paragraph.text_mut().unwrap().tags { | 2076 | for tag in &mut text.tags { |
| 1890 | tag.extra_set = 0; | 2077 | tag.extra_set = 0; |
| 1891 | } | 2078 | } |
| 1892 | } | 2079 | } |
| ... | @@ -1943,5 +2130,30 @@ mod tests { | ... | @@ -1943,5 +2130,30 @@ mod tests { |
| 1943 | tags: vec![NoteTag::ToDo, NoteTag::Question], | 2130 | tags: vec![NoteTag::ToDo, NoteTag::Question], |
| 1944 | } | 2131 | } |
| 1945 | ); | 2132 | ); |
| 2133 | // Format Painter, a gallery tag and an inserted table write as well. | ||
| 2134 | let painted = editor.painted_format().unwrap(); | ||
| 2135 | editor.select([at(0, 0), at(0, 2)].into()).unwrap(); | ||
| 2136 | editor | ||
| 2137 | .format(&mut engine, Formatting::Paint(painted)) | ||
| 2138 | .unwrap(); | ||
| 2139 | let state = editor.format_state().unwrap(); | ||
| 2140 | assert_eq!( | ||
| 2141 | (state.toggles, state.font_size, state.alignment), | ||
| 2142 | ( | ||
| 2143 | vec![Toggle::Bold, Toggle::Superscript], | ||
| 2144 | Some(14.0), | ||
| 2145 | Some(Alignment::Center) | ||
| 2146 | ) | ||
| 2147 | ); | ||
| 2148 | editor | ||
| 2149 | .format(&mut engine, Formatting::Tag(NoteTag::CallBack)) | ||
| 2150 | .unwrap(); | ||
| 2151 | editor.select([at(0, 1); 2].into()).unwrap(); | ||
| 2152 | editor.insert_table(&mut engine, 2, 3).unwrap(); | ||
| 2153 | assert_eq!(editor.selection(), Selection::from([at(1, 0); 2])); | ||
| 2154 | let edited = settled(editor.page().unwrap()); | ||
| 2155 | let written = super::ops::saved(&written, space, &mut editor); | ||
| 2156 | let (_, reread) = page(&written); | ||
| 2157 | assert_eq!(settled(reread).objects, edited.objects); | ||
| 1946 | } | 2158 | } |
| 1947 | } | 2159 | } |
crates/canvas/src/editor/table.rs+152| ... | @@ -78,6 +78,116 @@ fn cell_range(document: &TextDocument, cell: &TableCell) -> Result<Range<TextPos | ... | @@ -78,6 +78,116 @@ fn cell_range(document: &TextDocument, cell: &TableCell) -> Result<Range<TextPos |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | impl CanvasEditor { | 80 | impl CanvasEditor { |
| 81 | /// Insert, Table: an empty table of `rows` by `columns` at the caret with the caret in | ||
| 82 | /// its first cell, as OneNote 2010 inserts one: in place of an empty paragraph, before | ||
| 83 | /// or after the caret's paragraph at its start or end, and between its halves otherwise. | ||
| 84 | pub fn insert_table( | ||
| 85 | &mut self, | ||
| 86 | engine: &mut TextEngine, | ||
| 87 | rows: usize, | ||
| 88 | columns: usize, | ||
| 89 | ) -> Result<(), EditorError> { | ||
| 90 | let outline = self.active_outline(); | ||
| 91 | if outline.title || rows == 0 || columns == 0 { | ||
| 92 | return Ok(()); | ||
| 93 | } | ||
| 94 | let [anchor, focus] = outline.selection.positions; | ||
| 95 | let caret = anchor.max(focus); | ||
| 96 | let (container, index, node) = outline | ||
| 97 | .document | ||
| 98 | .leaf(caret.paragraph) | ||
| 99 | .ok_or(EditError::InvalidRange)?; | ||
| 100 | let text = &node.text().unwrap().text; | ||
| 101 | let end = text.utf16_offset(text.text().len())?; | ||
| 102 | let format = text.format_at(caret.offset)?; | ||
| 103 | let cell = || -> Result<TableCell, EditError> { | ||
| 104 | let mut paragraph = crate::document::node( | ||
| 105 | Paragraph::new(String::new(), format.clone()), | ||
| 106 | node.format.clone(), | ||
| 107 | )?; | ||
| 108 | paragraph.style = node.style; | ||
| 109 | Ok(TableCell { | ||
| 110 | id: new_id()?, | ||
| 111 | layout: Default::default(), | ||
| 112 | indents: outline.indents.clone(), | ||
| 113 | shading: None, | ||
| 114 | paragraphs: vec![paragraph], | ||
| 115 | unsupported: Vec::new(), | ||
| 116 | }) | ||
| 117 | }; | ||
| 118 | let table = PageParagraph { | ||
| 119 | id: new_id()?, | ||
| 120 | parent: node.parent, | ||
| 121 | level: node.level, | ||
| 122 | style: None, | ||
| 123 | format: node.format.clone(), | ||
| 124 | lists: Vec::new(), | ||
| 125 | tags: Vec::new(), | ||
| 126 | media: Default::default(), | ||
| 127 | collapsed: false, | ||
| 128 | content: ParagraphContent::Table(onestore::page::Table { | ||
| 129 | id: new_id()?, | ||
| 130 | columns: vec![ | ||
| 131 | TableColumn { | ||
| 132 | width: 37.11, | ||
| 133 | locked: false | ||
| 134 | }; | ||
| 135 | columns | ||
| 136 | ], | ||
| 137 | rows: (0..rows) | ||
| 138 | .map(|_| { | ||
| 139 | Ok(TableRow { | ||
| 140 | id: new_id()?, | ||
| 141 | cells: (0..columns).map(|_| cell()).collect::<Result<_, _>>()?, | ||
| 142 | }) | ||
| 143 | }) | ||
| 144 | .collect::<Result<_, EditError>>()?, | ||
| 145 | borders: Some(true), | ||
| 146 | layout: Default::default(), | ||
| 147 | tags: Vec::new(), | ||
| 148 | }), | ||
| 149 | }; | ||
| 150 | let parent = |candidate: &PageParagraph| candidate.parent == Some(node.id); | ||
| 151 | let childless = !outline.document.container(container)?.iter().any(parent); | ||
| 152 | let (edit, first) = if end == 0 && childless { | ||
| 153 | let edit = DocumentEdit { | ||
| 154 | columns: BTreeMap::new(), | ||
| 155 | container, | ||
| 156 | range: index..index + 1, | ||
| 157 | replacement: vec![table], | ||
| 158 | }; | ||
| 159 | (edit, caret.paragraph) | ||
| 160 | } else if caret.offset == 0 && end > 0 { | ||
| 161 | let edit = DocumentEdit { | ||
| 162 | columns: BTreeMap::new(), | ||
| 163 | container, | ||
| 164 | range: index..index, | ||
| 165 | replacement: vec![table], | ||
| 166 | }; | ||
| 167 | (edit, caret.paragraph) | ||
| 168 | } else if caret.offset == end { | ||
| 169 | let edit = DocumentEdit { | ||
| 170 | columns: BTreeMap::new(), | ||
| 171 | container, | ||
| 172 | range: index + 1..index + 1, | ||
| 173 | replacement: vec![table], | ||
| 174 | }; | ||
| 175 | (edit, caret.paragraph + 1) | ||
| 176 | } else { | ||
| 177 | let empty = Paragraph::new(String::new(), format.clone()); | ||
| 178 | let mut edit = outline | ||
| 179 | .document | ||
| 180 | .replace(caret..caret, vec![empty.clone(), empty])?; | ||
| 181 | edit.replacement.insert(1, table); | ||
| 182 | (edit, caret.paragraph + 1) | ||
| 183 | }; | ||
| 184 | let first = TextPosition { | ||
| 185 | paragraph: first, | ||
| 186 | offset: 0, | ||
| 187 | }; | ||
| 188 | self.commit(engine, edit, [first; 2].into()) | ||
| 189 | } | ||
| 190 | |||
| 81 | pub fn tab(&mut self, engine: &mut TextEngine, backward: bool) -> Result<(), EditorError> { | 191 | pub fn tab(&mut self, engine: &mut TextEngine, backward: bool) -> Result<(), EditorError> { |
| 82 | if self.active_outline().title && !backward { | 192 | if self.active_outline().title && !backward { |
| 83 | return self.leave_title(engine); | 193 | return self.leave_title(engine); |
| ... | @@ -399,6 +509,48 @@ mod tests { | ... | @@ -399,6 +509,48 @@ mod tests { |
| 399 | .collect() | 509 | .collect() |
| 400 | } | 510 | } |
| 401 | 511 | ||
| 512 | /// As OneNote 2010 inserts from the Table gallery (lab, 2026-09-28). | ||
| 513 | #[test] | ||
| 514 | fn an_inserted_table_takes_an_empty_paragraph_or_splits_at_the_caret() { | ||
| 515 | let mut engine = TextEngine::default(); | ||
| 516 | let texts = |editor: &CanvasEditor| { | ||
| 517 | editor | ||
| 518 | .active_outline() | ||
| 519 | .document | ||
| 520 | .nodes() | ||
| 521 | .iter() | ||
| 522 | .map(|node| match &node.content { | ||
| 523 | ParagraphContent::Table(table) => { | ||
| 524 | format!("{}x{}", table.columns.len(), table.rows.len()) | ||
| 525 | } | ||
| 526 | _ => node.text().unwrap().text.text().to_owned(), | ||
| 527 | }) | ||
| 528 | .collect::<Vec<_>>() | ||
| 529 | }; | ||
| 530 | for (text, offset, expected) in [ | ||
| 531 | ("", 0, vec!["3x2"]), | ||
| 532 | ("abc", 0, vec!["3x2", "abc"]), | ||
| 533 | ("abc", 3, vec!["abc", "3x2"]), | ||
| 534 | ("abc", 1, vec!["a", "3x2", "bc"]), | ||
| 535 | ] { | ||
| 536 | let mut editor = editor(&mut engine, text); | ||
| 537 | let caret = TextPosition { | ||
| 538 | paragraph: 0, | ||
| 539 | offset, | ||
| 540 | }; | ||
| 541 | editor.select([caret; 2].into()).unwrap(); | ||
| 542 | editor.insert_table(&mut engine, 2, 3).unwrap(); | ||
| 543 | assert_eq!(texts(&editor), expected, "{text:?} at {offset}"); | ||
| 544 | let first = usize::from(expected[0] != "3x2"); | ||
| 545 | let (cell, _, _) = editor | ||
| 546 | .active_outline() | ||
| 547 | .document | ||
| 548 | .leaf(editor.selection().positions[0].paragraph) | ||
| 549 | .unwrap(); | ||
| 550 | assert!(cell.is_some() && editor.selection().positions[0].paragraph == first); | ||
| 551 | } | ||
| 552 | } | ||
| 553 | |||
| 402 | #[test] | 554 | #[test] |
| 403 | fn tab_creates_splits_and_selects_native_cells() { | 555 | fn tab_creates_splits_and_selects_native_cells() { |
| 404 | let mut engine = TextEngine::default(); | 556 | let mut engine = TextEngine::default(); |
crates/canvas/src/interaction/mod.rs+199-45| ... | @@ -14,7 +14,7 @@ mod tests; | ... | @@ -14,7 +14,7 @@ mod tests; |
| 14 | use crate::gpu::{Paper, Viewport, page::PageScene}; | 14 | use crate::gpu::{Paper, Viewport, page::PageScene}; |
| 15 | use crate::{ | 15 | use crate::{ |
| 16 | date::DateField, | 16 | date::DateField, |
| 17 | editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, Formatting, Selection, TextOutline}, | 17 | editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, Formatting, Selection, TextOutline, Whole}, |
| 18 | layout::TextEngine, | 18 | layout::TextEngine, |
| 19 | }; | 19 | }; |
| 20 | use draw::{ | 20 | use draw::{ |
| ... | @@ -156,6 +156,7 @@ enum Drag { | ... | @@ -156,6 +156,7 @@ enum Drag { |
| 156 | #[derive(Clone, Copy)] | 156 | #[derive(Clone, Copy)] |
| 157 | enum PointerFeedback<'a> { | 157 | enum PointerFeedback<'a> { |
| 158 | Hover(onestore::ExGuid), | 158 | Hover(onestore::ExGuid), |
| 159 | /// An outline dragged this far, with the rest of the page selection it belongs to. | ||
| 159 | Move(onestore::ExGuid, [f32; 2]), | 160 | Move(onestore::ExGuid, [f32; 2]), |
| 160 | Resize(&'a TextOutline), | 161 | Resize(&'a TextOutline), |
| 161 | /// A picture being moved or resized, drawn at this origin and size. | 162 | /// A picture being moved or resized, drawn at this origin and size. |
| ... | @@ -384,6 +385,18 @@ impl PageView { | ... | @@ -384,6 +385,18 @@ impl PageView { |
| 384 | )) | 385 | )) |
| 385 | } | 386 | } |
| 386 | 387 | ||
| 388 | /// The dragged outline and how far it would move. | ||
| 389 | fn drag_delta(&self) -> Option<(onestore::ExGuid, [f32; 2])> { | ||
| 390 | let (id, [x, y]) = self.outline_preview()?; | ||
| 391 | let outline = self | ||
| 392 | .editor | ||
| 393 | .outlines() | ||
| 394 | .iter() | ||
| 395 | .find(|outline| outline.id == id)?; | ||
| 396 | let [from_x, from_y] = outline.origin(); | ||
| 397 | Some((id, [x - from_x, y - from_y])) | ||
| 398 | } | ||
| 399 | |||
| 387 | fn image_preview(&self) -> Option<(onestore::ExGuid, [f32; 2], [f32; 2])> { | 400 | fn image_preview(&self) -> Option<(onestore::ExGuid, [f32; 2], [f32; 2])> { |
| 388 | let Drag::Image { | 401 | let Drag::Image { |
| 389 | id, | 402 | id, |
| ... | @@ -701,8 +714,8 @@ impl PageView { | ... | @@ -701,8 +714,8 @@ impl PageView { |
| 701 | .. | 714 | .. |
| 702 | }) => Some(PointerFeedback::Resize(outline)), | 715 | }) => Some(PointerFeedback::Resize(outline)), |
| 703 | _ => self | 716 | _ => self |
| 704 | .outline_preview() | 717 | .drag_delta() |
| 705 | .map(|(id, origin)| PointerFeedback::Move(id, origin)) | 718 | .map(|(id, delta)| PointerFeedback::Move(id, delta)) |
| 706 | .or_else(|| { | 719 | .or_else(|| { |
| 707 | self.image_preview() | 720 | self.image_preview() |
| 708 | .map(|(id, origin, size)| PointerFeedback::Image(id, origin, size)) | 721 | .map(|(id, origin, size)| PointerFeedback::Image(id, origin, size)) |
| ... | @@ -901,7 +914,10 @@ impl PageView { | ... | @@ -901,7 +914,10 @@ impl PageView { |
| 901 | } | 914 | } |
| 902 | Some(Hit::Handle { id, grab }) => { | 915 | Some(Hit::Handle { id, grab }) => { |
| 903 | self.set_object_focus(None); | 916 | self.set_object_focus(None); |
| 904 | self.editor.focus_outline(id)?; | 917 | // Any outline's handle drags the whole page selection. |
| 918 | if self.editor.whole() != Some(Whole::Page) { | ||
| 919 | self.editor.focus_outline(id)?; | ||
| 920 | } | ||
| 905 | self.drag = Some(Drag::Outline { | 921 | self.drag = Some(Drag::Outline { |
| 906 | id, | 922 | id, |
| 907 | grab, | 923 | grab, |
| ... | @@ -975,7 +991,16 @@ impl PageView { | ... | @@ -975,7 +991,16 @@ impl PageView { |
| 975 | let inside = | 991 | let inside = |
| 976 | (0..2).all(|axis| (0.0..self.viewport.size[axis] as f32).contains(&self.pointer[axis])); | 992 | (0..2).all(|axis| (0.0..self.viewport.size[axis] as f32).contains(&self.pointer[axis])); |
| 977 | let moving_image = matches!(self.drag, Some(Drag::Image { handle: [0, 0], .. })); | 993 | let moving_image = matches!(self.drag, Some(Drag::Image { handle: [0, 0], .. })); |
| 994 | let clicked = match self.drag { | ||
| 995 | Some(Drag::Outline { | ||
| 996 | id, | ||
| 997 | pending_press: Some(_), | ||
| 998 | .. | ||
| 999 | }) => Some(id), | ||
| 1000 | _ => None, | ||
| 1001 | }; | ||
| 978 | let preview = self.outline_preview().filter(|_| inside); | 1002 | let preview = self.outline_preview().filter(|_| inside); |
| 1003 | let delta = self.drag_delta().filter(|_| inside); | ||
| 979 | if let Some((id, origin, size)) = self.image_preview().filter(|_| inside || !moving_image) { | 1004 | if let Some((id, origin, size)) = self.image_preview().filter(|_| inside || !moving_image) { |
| 980 | self.editor | 1005 | self.editor |
| 981 | .place_image(&mut self.engine, id, origin, size)?; | 1006 | .place_image(&mut self.engine, id, origin, size)?; |
| ... | @@ -988,7 +1013,14 @@ impl PageView { | ... | @@ -988,7 +1013,14 @@ impl PageView { |
| 988 | self.editor | 1013 | self.editor |
| 989 | .resize(&mut self.engine, outline.bounds().width() as f32)?; | 1014 | .resize(&mut self.engine, outline.bounds().width() as f32)?; |
| 990 | } | 1015 | } |
| 991 | if let Some((id, origin)) = preview { | 1016 | if let Some(id) = clicked { |
| 1017 | // A click on an outline's handle selects it as OneNote does. | ||
| 1018 | self.editor.select_outline(id)?; | ||
| 1019 | } else if let Some((_, delta)) = delta | ||
| 1020 | && self.editor.whole() == Some(Whole::Page) | ||
| 1021 | { | ||
| 1022 | self.editor.move_page(delta)?; | ||
| 1023 | } else if let Some((id, origin)) = preview { | ||
| 992 | self.editor.move_outline(id, origin)?; | 1024 | self.editor.move_outline(id, origin)?; |
| 993 | } | 1025 | } |
| 994 | self.changed() | 1026 | self.changed() |
| ... | @@ -1075,27 +1107,49 @@ impl PageView { | ... | @@ -1075,27 +1107,49 @@ impl PageView { |
| 1075 | self.edited() | 1107 | self.edited() |
| 1076 | } | 1108 | } |
| 1077 | 1109 | ||
| 1078 | /// Copy, or Cut with `cut`: the selected text goes to the clipboard. | 1110 | /// Copy, or Cut with `cut`: the selected text goes to the clipboard, the page selection's |
| 1111 | /// outlines top to bottom with a blank line between, as OneNote 2010 copies them. | ||
| 1079 | pub fn copy(&mut self, cut: bool) -> Result<Response> { | 1112 | pub fn copy(&mut self, cut: bool) -> Result<Response> { |
| 1080 | let [anchor, focus] = self.editor.selection().positions; | 1113 | let text = |outline: &TextOutline, selection: Selection| -> Result<String> { |
| 1081 | let selected = self | 1114 | let [anchor, focus] = selection.positions; |
| 1082 | .editor | 1115 | Ok(outline |
| 1083 | .active_outline() | 1116 | .document() |
| 1084 | .document() | 1117 | .slice(anchor.min(focus)..anchor.max(focus))? |
| 1085 | .slice(anchor.min(focus)..anchor.max(focus))?; | 1118 | .iter() |
| 1086 | let text = selected | 1119 | .map(|paragraph| { |
| 1087 | .iter() | 1120 | paragraph |
| 1088 | .map(|paragraph| { | 1121 | .project() |
| 1089 | paragraph | 1122 | .map(|projection| projection.text().text().to_owned()) |
| 1090 | .project() | 1123 | }) |
| 1091 | .map(|projection| projection.text().text().to_owned()) | 1124 | .collect::<std::result::Result<Vec<_>, _>>()? |
| 1092 | }) | 1125 | .join("\n")) |
| 1093 | .collect::<std::result::Result<Vec<_>, _>>()? | 1126 | }; |
| 1094 | .join("\n"); | 1127 | let page = self.editor.whole() == Some(Whole::Page); |
| 1128 | let text = if page { | ||
| 1129 | let mut outlines = self | ||
| 1130 | .editor | ||
| 1131 | .outlines() | ||
| 1132 | .iter() | ||
| 1133 | .filter(|outline| !outline.title) | ||
| 1134 | .collect::<Vec<_>>(); | ||
| 1135 | outlines.sort_by(|a, b| { | ||
| 1136 | let ([ax, ay], [bx, by]) = (a.origin(), b.origin()); | ||
| 1137 | ay.total_cmp(&by).then(ax.total_cmp(&bx)) | ||
| 1138 | }); | ||
| 1139 | outlines | ||
| 1140 | .into_iter() | ||
| 1141 | .map(|outline| text(outline, outline.whole())) | ||
| 1142 | .collect::<Result<Vec<_>>>()? | ||
| 1143 | .join("\n\n") | ||
| 1144 | } else { | ||
| 1145 | text(self.editor.active_outline(), self.editor.selection())? | ||
| 1146 | }; | ||
| 1095 | if text.is_empty() { | 1147 | if text.is_empty() { |
| 1096 | return Ok(Response::default()); | 1148 | return Ok(Response::default()); |
| 1097 | } | 1149 | } |
| 1098 | if cut { | 1150 | if cut && page { |
| 1151 | self.editor.delete(&mut self.engine, false)?; | ||
| 1152 | } else if cut { | ||
| 1099 | self.editor.insert(&mut self.engine, "")?; | 1153 | self.editor.insert(&mut self.engine, "")?; |
| 1100 | } | 1154 | } |
| 1101 | Ok(Response { | 1155 | Ok(Response { |
| ... | @@ -1177,6 +1231,36 @@ impl PageView { | ... | @@ -1177,6 +1231,36 @@ impl PageView { |
| 1177 | self.edited() | 1231 | self.edited() |
| 1178 | } | 1232 | } |
| 1179 | 1233 | ||
| 1234 | /// Insert, Table: see [`CanvasEditor::insert_table`]. | ||
| 1235 | pub fn insert_table(&mut self, rows: usize, columns: usize) -> Result<Response> { | ||
| 1236 | if !self.accepts_text() || self.editor.marked_range().is_some() { | ||
| 1237 | return Ok(Response::default()); | ||
| 1238 | } | ||
| 1239 | self.editor.insert_table(&mut self.engine, rows, columns)?; | ||
| 1240 | self.edited() | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | /// Insert, Picture: a PNG, JPEG or GIF `size` points large after the caret's paragraph. | ||
| 1244 | pub fn insert_picture(&mut self, bytes: Vec<u8>, size: [f32; 2]) -> Result<Response> { | ||
| 1245 | if !self.accepts_text() || self.editor.marked_range().is_some() { | ||
| 1246 | return Ok(Response::default()); | ||
| 1247 | } | ||
| 1248 | let image = onestore::page::Image { | ||
| 1249 | id: onestore::page::text::new_id()?, | ||
| 1250 | layout: onestore::document::Layout { | ||
| 1251 | max_width: Some(size[0]), | ||
| 1252 | max_height: Some(size[1]), | ||
| 1253 | ..Default::default() | ||
| 1254 | }, | ||
| 1255 | size: Some(size), | ||
| 1256 | bytes: Some(bytes.into()), | ||
| 1257 | alt: None, | ||
| 1258 | background: false, | ||
| 1259 | }; | ||
| 1260 | self.editor.insert_picture(&mut self.engine, image)?; | ||
| 1261 | self.edited() | ||
| 1262 | } | ||
| 1263 | |||
| 1180 | /// Professional, or Linear with `linear`, on the equation at the caret. | 1264 | /// Professional, or Linear with `linear`, on the equation at the caret. |
| 1181 | pub fn switch_equation(&mut self, linear: bool) -> Result<Response> { | 1265 | pub fn switch_equation(&mut self, linear: bool) -> Result<Response> { |
| 1182 | if linear { | 1266 | if linear { |
| ... | @@ -1205,6 +1289,10 @@ impl PageView { | ... | @@ -1205,6 +1289,10 @@ impl PageView { |
| 1205 | /// Select All, widening the selection a unit at a time as OneNote 2010's Ctrl+A does. | 1289 | /// Select All, widening the selection a unit at a time as OneNote 2010's Ctrl+A does. |
| 1206 | pub fn widen_selection(&mut self) -> Result<Response> { | 1290 | pub fn widen_selection(&mut self) -> Result<Response> { |
| 1207 | self.editor.widen_selection()?; | 1291 | self.editor.widen_selection()?; |
| 1292 | // Revealing the last outline would scroll the page selection's start away. | ||
| 1293 | if self.editor.whole() == Some(Whole::Page) { | ||
| 1294 | return self.changed(); | ||
| 1295 | } | ||
| 1208 | self.edited() | 1296 | self.edited() |
| 1209 | } | 1297 | } |
| 1210 | 1298 | ||
| ... | @@ -1274,6 +1362,27 @@ impl PageView { | ... | @@ -1274,6 +1362,27 @@ impl PageView { |
| 1274 | return self.changed(); | 1362 | return self.changed(); |
| 1275 | } | 1363 | } |
| 1276 | } | 1364 | } |
| 1365 | if self.editor.whole() == Some(Whole::Page) { | ||
| 1366 | match (Command::from_key(key, self.modifiers), key) { | ||
| 1367 | // OneNote 2010 keeps its page selection on these. | ||
| 1368 | ( | ||
| 1369 | Some( | ||
| 1370 | Command::Move(Movement::Up | Movement::Down) | ||
| 1371 | | Command::MovePage { .. } | ||
| 1372 | | Command::MoveParagraphs { .. }, | ||
| 1373 | ), | ||
| 1374 | _, | ||
| 1375 | ) | ||
| 1376 | | (_, Key::Named(NamedKey::Tab)) => return Ok(Response::default()), | ||
| 1377 | // Enter leaves the caret at the selection's end, as Right does. | ||
| 1378 | (_, Key::Named(NamedKey::Enter)) => { | ||
| 1379 | self.editor | ||
| 1380 | .move_selection(&mut self.engine, Movement::Right, false)?; | ||
| 1381 | return self.edited(); | ||
| 1382 | } | ||
| 1383 | _ => {} | ||
| 1384 | } | ||
| 1385 | } | ||
| 1277 | if let Some(Command::MoveParagraphs { up }) = Command::from_key(key, self.modifiers) | 1386 | if let Some(Command::MoveParagraphs { up }) = Command::from_key(key, self.modifiers) |
| 1278 | && self.editor.marked_range().is_none() | 1387 | && self.editor.marked_range().is_none() |
| 1279 | { | 1388 | { |
| ... | @@ -1288,6 +1397,13 @@ impl PageView { | ... | @@ -1288,6 +1397,13 @@ impl PageView { |
| 1288 | Key::Named(NamedKey::ArrowDown) => Some([0.0, 1.0]), | 1397 | Key::Named(NamedKey::ArrowDown) => Some([0.0, 1.0]), |
| 1289 | _ => None, | 1398 | _ => None, |
| 1290 | }; | 1399 | }; |
| 1400 | if let Some(delta) = delta | ||
| 1401 | && self.editor.whole() == Some(Whole::Page) | ||
| 1402 | { | ||
| 1403 | let step = if shift { 10.0 } else { 1.0 }; | ||
| 1404 | self.editor.move_page(delta.map(|axis| axis * step))?; | ||
| 1405 | return self.edited(); | ||
| 1406 | } | ||
| 1291 | if let Some(delta) = delta { | 1407 | if let Some(delta) = delta { |
| 1292 | let outline = self.editor.active_outline(); | 1408 | let outline = self.editor.active_outline(); |
| 1293 | let origin = outline.origin(); | 1409 | let origin = outline.origin(); |
| ... | @@ -1604,6 +1720,15 @@ fn page_primitives<'a>( | ... | @@ -1604,6 +1720,15 @@ fn page_primitives<'a>( |
| 1604 | paint: Paint, | 1720 | paint: Paint, |
| 1605 | ) -> Result<Vec<Primitive<'a>>> { | 1721 | ) -> Result<Vec<Primitive<'a>>> { |
| 1606 | let mut primitives = Vec::new(); | 1722 | let mut primitives = Vec::new(); |
| 1723 | let active = editor.active_outline().id; | ||
| 1724 | let whole = editor | ||
| 1725 | .whole() | ||
| 1726 | .filter(|_| paint.chrome && object_focus.is_none()); | ||
| 1727 | let selected = |outline: &TextOutline| match whole { | ||
| 1728 | Some(Whole::Page) => !outline.title, | ||
| 1729 | Some(Whole::Outline) => outline.id == active, | ||
| 1730 | None => false, | ||
| 1731 | }; | ||
| 1607 | let draw_outline = |id, offset: [f32; 2], primitives: &mut Vec<_>| { | 1732 | let draw_outline = |id, offset: [f32; 2], primitives: &mut Vec<_>| { |
| 1608 | // An emptied page outline leaves the editor but keeps its paint slot for undo. | 1733 | // An emptied page outline leaves the editor but keeps its paint slot for undo. |
| 1609 | let Some(outline) = editor.visible_outlines().find(|outline| outline.id == id) else { | 1734 | let Some(outline) = editor.visible_outlines().find(|outline| outline.id == id) else { |
| ... | @@ -1613,13 +1738,19 @@ fn page_primitives<'a>( | ... | @@ -1613,13 +1738,19 @@ fn page_primitives<'a>( |
| 1613 | Some(PointerFeedback::Resize(resized)) if resized.id == outline.id => resized, | 1738 | Some(PointerFeedback::Resize(resized)) if resized.id == outline.id => resized, |
| 1614 | _ => outline, | 1739 | _ => outline, |
| 1615 | }; | 1740 | }; |
| 1741 | let [x, y] = outline.origin(); | ||
| 1616 | let origin = match preview { | 1742 | let origin = match preview { |
| 1617 | Some(PointerFeedback::Move(id, origin)) if id == outline.id => origin, | 1743 | Some(PointerFeedback::Move(id, [dx, dy])) |
| 1618 | _ => outline.origin(), | 1744 | if id == outline.id || whole == Some(Whole::Page) && !outline.title => |
| 1745 | { | ||
| 1746 | [x + dx, y + dy] | ||
| 1747 | } | ||
| 1748 | _ => [x, y], | ||
| 1619 | }; | 1749 | }; |
| 1620 | // OneNote frames the title whether or not it is focused or hovered. | 1750 | // OneNote frames the title whether or not it is focused or hovered. |
| 1621 | if outline.title | 1751 | if outline.title |
| 1622 | || (paint.chrome && object_focus.is_none() && outline.id == editor.active_outline().id) | 1752 | || selected(outline) |
| 1753 | || (paint.chrome && object_focus.is_none() && outline.id == active) | ||
| 1623 | || matches!(preview, Some(PointerFeedback::Hover(id) | PointerFeedback::Move(id, _)) if id == outline.id) | 1754 | || matches!(preview, Some(PointerFeedback::Hover(id) | PointerFeedback::Move(id, _)) if id == outline.id) |
| 1624 | || matches!(preview, Some(PointerFeedback::Resize(resized)) if resized.id == outline.id) | 1755 | || matches!(preview, Some(PointerFeedback::Resize(resized)) if resized.id == outline.id) |
| 1625 | { | 1756 | { |
| ... | @@ -1628,14 +1759,16 @@ fn page_primitives<'a>( | ... | @@ -1628,14 +1759,16 @@ fn page_primitives<'a>( |
| 1628 | [origin[0] + offset[0], origin[1] + offset[1]], | 1759 | [origin[0] + offset[0], origin[1] + offset[1]], |
| 1629 | paint.pixel, | 1760 | paint.pixel, |
| 1630 | paint.colors.paper, | 1761 | paint.colors.paper, |
| 1762 | selected(outline), | ||
| 1631 | primitives, | 1763 | primitives, |
| 1632 | ); | 1764 | ); |
| 1633 | } | 1765 | } |
| 1634 | append_outline( | 1766 | append_outline( |
| 1635 | (object_focus.is_none() | 1767 | (object_focus.is_none() |
| 1636 | && outline.id == editor.active_outline().id | 1768 | && outline.id == active |
| 1637 | && !matches!(preview, Some(PointerFeedback::Resize(_)))) | 1769 | && !matches!(preview, Some(PointerFeedback::Resize(_)))) |
| 1638 | .then_some(editor), | 1770 | .then_some(editor), |
| 1771 | selected(outline), | ||
| 1639 | outline, | 1772 | outline, |
| 1640 | [origin[0] + offset[0], origin[1] + offset[1]], | 1773 | [origin[0] + offset[0], origin[1] + offset[1]], |
| 1641 | paint, | 1774 | paint, |
| ... | @@ -1692,6 +1825,7 @@ fn page_primitives<'a>( | ... | @@ -1692,6 +1825,7 @@ fn page_primitives<'a>( |
| 1692 | { | 1825 | { |
| 1693 | append_outline( | 1826 | append_outline( |
| 1694 | Some(editor), | 1827 | Some(editor), |
| 1828 | false, | ||
| 1695 | outline, | 1829 | outline, |
| 1696 | outline.origin(), | 1830 | outline.origin(), |
| 1697 | paint, | 1831 | paint, |
| ... | @@ -1892,22 +2026,26 @@ fn snap_to_grid(point: [f32; 2], margin: [f32; 2]) -> [f32; 2] { | ... | @@ -1892,22 +2026,26 @@ fn snap_to_grid(point: [f32; 2], margin: [f32; 2]) -> [f32; 2] { |
| 1892 | 2026 | ||
| 1893 | fn outline_chrome(outline: &TextOutline, pixel: f32) -> ([f32; 4], f32) { | 2027 | fn outline_chrome(outline: &TextOutline, pixel: f32) -> ([f32; 4], f32) { |
| 1894 | let bounds = outline.bounds(); | 2028 | let bounds = outline.bounds(); |
| 1895 | // Native chrome combines page-scaled gutters with a fixed screen inset. | ||
| 1896 | let inset = 5.0 * pixel; | ||
| 1897 | let body_top = bounds.y0 as f32 - inset; | ||
| 1898 | if outline.title { | 2029 | if outline.title { |
| 2030 | // The title's box scales with the page, 5 px from its text at 100% (6 px more to the | ||
| 2031 | // left, 2 px less to the right). | ||
| 2032 | let inset = 3.75; | ||
| 1899 | let (_, paragraph) = outline.layouts().last().unwrap(); | 2033 | let (_, paragraph) = outline.layouts().last().unwrap(); |
| 1900 | let bottom = outline.origin()[1] + paragraph.origin[1] + paragraph.text.height(); | 2034 | let bottom = outline.origin()[1] + paragraph.origin[1] + paragraph.text.height(); |
| 2035 | let top = bounds.y0 as f32 - inset; | ||
| 1901 | return ( | 2036 | return ( |
| 1902 | [ | 2037 | [ |
| 1903 | bounds.x0 as f32 - inset - 6.0 * pixel, | 2038 | bounds.x0 as f32 - inset - 4.5, |
| 1904 | body_top, | 2039 | top, |
| 1905 | bounds.x1 as f32 + inset - 2.0 * pixel, | 2040 | bounds.x1 as f32 + inset - 1.5, |
| 1906 | bottom + inset, | 2041 | bottom + inset, |
| 1907 | ], | 2042 | ], |
| 1908 | body_top, | 2043 | top, |
| 1909 | ); | 2044 | ); |
| 1910 | } | 2045 | } |
| 2046 | // Native chrome combines page-scaled gutters with a fixed screen inset. | ||
| 2047 | let inset = 5.0 * pixel; | ||
| 2048 | let body_top = bounds.y0 as f32 - inset; | ||
| 1911 | // OneNote widens the box leftward to hold the tag column and list markers; text and right | 2049 | // OneNote widens the box leftward to hold the tag column and list markers; text and right |
| 1912 | // edge stay. | 2050 | // edge stay. |
| 1913 | let shaped = outline.shaped(); | 2051 | let shaped = outline.shaped(); |
| ... | @@ -1933,11 +2071,13 @@ fn outline_chrome(outline: &TextOutline, pixel: f32) -> ([f32; 4], f32) { | ... | @@ -1933,11 +2071,13 @@ fn outline_chrome(outline: &TextOutline, pixel: f32) -> ([f32; 4], f32) { |
| 1933 | ) | 2071 | ) |
| 1934 | } | 2072 | } |
| 1935 | 2073 | ||
| 2074 | /// `selected` greys the body, as OneNote marks an outline Select All has selected whole. | ||
| 1936 | fn append_outline_chrome( | 2075 | fn append_outline_chrome( |
| 1937 | outline: &TextOutline, | 2076 | outline: &TextOutline, |
| 1938 | origin: [f32; 2], | 2077 | origin: [f32; 2], |
| 1939 | pixel: f32, | 2078 | pixel: f32, |
| 1940 | paper: crate::gpu::Paper, | 2079 | paper: crate::gpu::Paper, |
| 2080 | selected: bool, | ||
| 1941 | primitives: &mut Vec<Primitive<'_>>, | 2081 | primitives: &mut Vec<Primitive<'_>>, |
| 1942 | ) { | 2082 | ) { |
| 1943 | let [x, y] = origin; | 2083 | let [x, y] = origin; |
| ... | @@ -1973,6 +2113,14 @@ fn append_outline_chrome( | ... | @@ -1973,6 +2113,14 @@ fn append_outline_chrome( |
| 1973 | stroke: None, | 2113 | stroke: None, |
| 1974 | color: paper.shade(crate::gpu::colorref(0x00e5dee7)), | 2114 | color: paper.shade(crate::gpu::colorref(0x00e5dee7)), |
| 1975 | }); | 2115 | }); |
| 2116 | if selected { | ||
| 2117 | primitives.push(Primitive::RoundedRect { | ||
| 2118 | rect: [left, body_top, right, bottom], | ||
| 2119 | radius: [3.0 * pixel; 2], | ||
| 2120 | stroke: None, | ||
| 2121 | color: paper.shade(crate::gpu::colorref(0x00f0f0f0)), | ||
| 2122 | }); | ||
| 2123 | } | ||
| 1976 | primitives.push(Primitive::RoundedRect { | 2124 | primitives.push(Primitive::RoundedRect { |
| 1977 | rect: [left, top, right, bottom], | 2125 | rect: [left, top, right, bottom], |
| 1978 | radius: [3.0 * pixel; 2], | 2126 | radius: [3.0 * pixel; 2], |
| ... | @@ -2008,8 +2156,11 @@ fn append_outline_chrome( | ... | @@ -2008,8 +2156,11 @@ fn append_outline_chrome( |
| 2008 | } | 2156 | } |
| 2009 | } | 2157 | } |
| 2010 | 2158 | ||
| 2159 | /// `editor` paints its selection and caret in the focused outline; `selected` highlights all | ||
| 2160 | /// the text of another. | ||
| 2011 | fn append_outline<'a>( | 2161 | fn append_outline<'a>( |
| 2012 | editor: Option<&'a CanvasEditor>, | 2162 | editor: Option<&'a CanvasEditor>, |
| 2163 | selected: bool, | ||
| 2013 | outline: &'a TextOutline, | 2164 | outline: &'a TextOutline, |
| 2014 | origin: [f32; 2], | 2165 | origin: [f32; 2], |
| 2015 | paint: Paint, | 2166 | paint: Paint, |
| ... | @@ -2052,18 +2203,21 @@ fn append_outline<'a>( | ... | @@ -2052,18 +2203,21 @@ fn append_outline<'a>( |
| 2052 | }); | 2203 | }); |
| 2053 | } | 2204 | } |
| 2054 | } | 2205 | } |
| 2055 | if let Some(editor) = editor { | 2206 | let highlight = match editor { |
| 2056 | for rect in editor.selection_rects()? { | 2207 | Some(editor) => editor.selection_rects()?, |
| 2057 | primitives.push(Primitive::Rect { | 2208 | None if selected => outline.range_rects(outline.whole())?, |
| 2058 | rect: [ | 2209 | None => Vec::new(), |
| 2059 | rect.x0 as f32 + x, | 2210 | }; |
| 2060 | rect.y0 as f32 + y, | 2211 | for rect in highlight { |
| 2061 | rect.x1 as f32 + x, | 2212 | primitives.push(Primitive::Rect { |
| 2062 | rect.y1 as f32 + y, | 2213 | rect: [ |
| 2063 | ], | 2214 | rect.x0 as f32 + x, |
| 2064 | color: colors.selection, | 2215 | rect.y0 as f32 + y, |
| 2065 | }); | 2216 | rect.x1 as f32 + x, |
| 2066 | } | 2217 | rect.y1 as f32 + y, |
| 2218 | ], | ||
| 2219 | color: colors.selection, | ||
| 2220 | }); | ||
| 2067 | } | 2221 | } |
| 2068 | for (index, paragraph) in outline.shaped().visible(rows) { | 2222 | for (index, paragraph) in outline.shaped().visible(rows) { |
| 2069 | outline.shaped().append_paragraph_primitives( | 2223 | outline.shaped().append_paragraph_primitives( |
crates/canvas/src/interaction/tests.rs+362-2| ... | @@ -1,9 +1,11 @@ | ... | @@ -1,9 +1,11 @@ |
| 1 | use super::*; | 1 | use super::*; |
| 2 | use crate::document::TextDocument; | 2 | use crate::document::{TextDocument, TextPosition}; |
| 3 | use crate::editor::Toggle; | ||
| 3 | use crate::gpu::painted_layout; | 4 | use crate::gpu::painted_layout; |
| 4 | use onestore::document::Format; | 5 | use onestore::document::Format; |
| 5 | use onestore::page::Page; | 6 | use onestore::page::Page; |
| 6 | use onestore::page::text::Paragraph; | 7 | use onestore::page::text::Paragraph; |
| 8 | use std::collections::BTreeSet; | ||
| 7 | use std::sync::Arc; | 9 | use std::sync::Arc; |
| 8 | 10 | ||
| 9 | const COLORS: TextColors = TextColors { | 11 | const COLORS: TextColors = TextColors { |
| ... | @@ -143,6 +145,11 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() { | ... | @@ -143,6 +145,11 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() { |
| 143 | assert_eq!(editor.active_outline().bounds().width(), 162.0); | 145 | assert_eq!(editor.active_outline().bounds().width(), 162.0); |
| 144 | assert_eq!(editor.active_outline().snapshot().min_width, Some(162.0)); | 146 | assert_eq!(editor.active_outline().snapshot().min_width, Some(162.0)); |
| 145 | let (native_rect, _) = outline_chrome(editor.active_outline(), 0.75); | 147 | let (native_rect, _) = outline_chrome(editor.active_outline(), 0.75); |
| 148 | // Unlike a body outline's, the title's box scales with zoom (OneNote 2010 at 200%). | ||
| 149 | assert_eq!( | ||
| 150 | outline_chrome(editor.active_outline(), 0.375).0, | ||
| 151 | native_rect | ||
| 152 | ); | ||
| 146 | for (axis, (actual, expected)) in native_rect | 153 | for (axis, (actual, expected)) in native_rect |
| 147 | .iter() | 154 | .iter() |
| 148 | .zip([85.0, 98.0, 315.0, 134.0]) | 155 | .zip([85.0, 98.0, 315.0, 134.0]) |
| ... | @@ -621,7 +628,7 @@ fn chrome_follows_focus_hover_and_drag_without_changing_hit_geometry() { | ... | @@ -621,7 +628,7 @@ fn chrome_follows_focus_hover_and_drag_without_changing_hit_geometry() { |
| 621 | assert_eq!(borders(None).len(), 1); | 628 | assert_eq!(borders(None).len(), 1); |
| 622 | let hovered = borders(Some(PointerFeedback::Hover(second))); | 629 | let hovered = borders(Some(PointerFeedback::Hover(second))); |
| 623 | assert_eq!(hovered.len(), 2); | 630 | assert_eq!(hovered.len(), 2); |
| 624 | let moved = borders(Some(PointerFeedback::Move(second, [240.0, 50.0]))); | 631 | let moved = borders(Some(PointerFeedback::Move(second, [20.0, 50.0]))); |
| 625 | assert_eq!(moved[1][0] - hovered[1][0], 20.0); | 632 | assert_eq!(moved[1][0] - hovered[1][0], 20.0); |
| 626 | assert_eq!(moved[1][1] - hovered[1][1], 50.0); | 633 | assert_eq!(moved[1][1] - hovered[1][1], 50.0); |
| 627 | assert!( | 634 | assert!( |
| ... | @@ -868,6 +875,7 @@ fn table_glyphs_highlights_and_selection_share_cell_paint_bounds() { | ... | @@ -868,6 +875,7 @@ fn table_glyphs_highlights_and_selection_share_cell_paint_bounds() { |
| 868 | let mut primitives = Vec::new(); | 875 | let mut primitives = Vec::new(); |
| 869 | append_outline( | 876 | append_outline( |
| 870 | Some(&editor), | 877 | Some(&editor), |
| 878 | false, | ||
| 871 | editor.active_outline(), | 879 | editor.active_outline(), |
| 872 | [24.0, 48.0], | 880 | [24.0, 48.0], |
| 873 | Paint { | 881 | Paint { |
| ... | @@ -941,6 +949,7 @@ fn editable_tables_paint_borders_before_selection_and_cell_text() { | ... | @@ -941,6 +949,7 @@ fn editable_tables_paint_borders_before_selection_and_cell_text() { |
| 941 | let mut primitives = Vec::new(); | 949 | let mut primitives = Vec::new(); |
| 942 | append_outline( | 950 | append_outline( |
| 943 | Some(&editor), | 951 | Some(&editor), |
| 952 | false, | ||
| 944 | editor.active_outline(), | 953 | editor.active_outline(), |
| 945 | [24.0, 48.0], | 954 | [24.0, 48.0], |
| 946 | Paint { | 955 | Paint { |
| ... | @@ -2065,3 +2074,354 @@ fn an_empty_preedit_without_a_composition_changes_nothing() { | ... | @@ -2065,3 +2074,354 @@ fn an_empty_preedit_without_a_composition_changes_nothing() { |
| 2065 | assert!(view.compose(String::new(), None).unwrap().changed); | 2074 | assert!(view.compose(String::new(), None).unwrap().changed); |
| 2066 | assert!(view.editor.marked_range().is_none()); | 2075 | assert!(view.editor.marked_range().is_none()); |
| 2067 | } | 2076 | } |
| 2077 | |||
| 2078 | /// The lab's page: a title, then outlines placed in the order Alpha, Beta, Gamma, Delta, with | ||
| 2079 | /// Delta highest on the page, drawn at 1 point per pixel. | ||
| 2080 | fn page_selection_view() -> (PageView, [onestore::ExGuid; 4]) { | ||
| 2081 | let mut engine = TextEngine::default(); | ||
| 2082 | let outline = |engine: &mut TextEngine, text: &str, origin| { | ||
| 2083 | let paragraphs = text | ||
| 2084 | .split('\n') | ||
| 2085 | .map(|line| Paragraph::new(line.into(), Default::default())) | ||
| 2086 | .collect(); | ||
| 2087 | TextOutline::new( | ||
| 2088 | engine, | ||
| 2089 | TextDocument::new(paragraphs).unwrap(), | ||
| 2090 | 240.0, | ||
| 2091 | origin, | ||
| 2092 | ) | ||
| 2093 | .unwrap() | ||
| 2094 | .snapshot() | ||
| 2095 | }; | ||
| 2096 | let mut title = outline(&mut engine, "Multi", [0.0; 2]); | ||
| 2097 | title.title = true; | ||
| 2098 | let body = [ | ||
| 2099 | ("Alpha one\nAlpha two", [36.0, 90.0]), | ||
| 2100 | ("Beta", [300.0, 180.0]), | ||
| 2101 | ("Gamma long line of text", [90.0, 300.0]), | ||
| 2102 | ("Delta", [380.0, 72.0]), | ||
| 2103 | ] | ||
| 2104 | .map(|(text, origin)| outline(&mut engine, text, origin)); | ||
| 2105 | let ids = body.each_ref().map(|outline| outline.id); | ||
| 2106 | let page = Page { | ||
| 2107 | title: "Multi".into(), | ||
| 2108 | identity: None, | ||
| 2109 | created: None, | ||
| 2110 | margin_origin: [36.0, 14.4], | ||
| 2111 | color: None, | ||
| 2112 | definitions: Default::default(), | ||
| 2113 | objects: std::iter::once(onestore::page::PageObject::Title(onestore::page::Title { | ||
| 2114 | id: onestore::page::text::new_id().unwrap(), | ||
| 2115 | date: None, | ||
| 2116 | layout: Default::default(), | ||
| 2117 | outlines: vec![title], | ||
| 2118 | })) | ||
| 2119 | .chain(body.map(onestore::page::PageObject::Outline)) | ||
| 2120 | .collect(), | ||
| 2121 | }; | ||
| 2122 | let (scene, editor) = PageScene::from_page(page, &mut engine).unwrap(); | ||
| 2123 | let mut view = PageView::new( | ||
| 2124 | editor, | ||
| 2125 | engine, | ||
| 2126 | Some((scene, [0.0; 2])), | ||
| 2127 | [800, 600], | ||
| 2128 | 1.0, | ||
| 2129 | Duration::from_millis(500), | ||
| 2130 | ); | ||
| 2131 | view.viewport.scale = 1.0; | ||
| 2132 | view.viewport.origin = [0.0; 2]; | ||
| 2133 | (view, ids) | ||
| 2134 | } | ||
| 2135 | |||
| 2136 | const START: TextPosition = TextPosition { | ||
| 2137 | paragraph: 0, | ||
| 2138 | offset: 0, | ||
| 2139 | }; | ||
| 2140 | |||
| 2141 | fn select_page(view: &mut PageView, from: onestore::ExGuid) { | ||
| 2142 | view.editor.focus_outline(from).unwrap(); | ||
| 2143 | view.editor.select([START; 2].into()).unwrap(); | ||
| 2144 | while view.editor.whole() != Some(Whole::Page) { | ||
| 2145 | let _ = view.widen_selection().unwrap(); | ||
| 2146 | } | ||
| 2147 | } | ||
| 2148 | |||
| 2149 | fn origins(view: &PageView) -> Vec<[f32; 2]> { | ||
| 2150 | let body = view | ||
| 2151 | .editor | ||
| 2152 | .outlines() | ||
| 2153 | .iter() | ||
| 2154 | .filter(|outline| !outline.title); | ||
| 2155 | body.map(TextOutline::origin).collect() | ||
| 2156 | } | ||
| 2157 | |||
| 2158 | /// As OneNote 2010 widens Ctrl+A past an outline (lab, 2026-09-28): a body outline's | ||
| 2159 | /// paragraph, the outline as an object, then every body outline, focusing the last. | ||
| 2160 | #[test] | ||
| 2161 | fn select_all_widens_from_an_outline_to_the_page() { | ||
| 2162 | let (mut view, [alpha, beta, _, delta]) = page_selection_view(); | ||
| 2163 | let levels = |view: &mut PageView, from| { | ||
| 2164 | view.editor.focus_outline(from).unwrap(); | ||
| 2165 | view.editor.select([START; 2].into()).unwrap(); | ||
| 2166 | (0..4) | ||
| 2167 | .map(|_| { | ||
| 2168 | let _ = view.widen_selection().unwrap(); | ||
| 2169 | (view.editor.whole(), view.editor.active_outline().id) | ||
| 2170 | }) | ||
| 2171 | .collect::<Vec<_>>() | ||
| 2172 | }; | ||
| 2173 | let page = (Some(Whole::Page), delta); | ||
| 2174 | for from in [alpha, beta] { | ||
| 2175 | assert_eq!( | ||
| 2176 | levels(&mut view, from), | ||
| 2177 | [(None, from), (Some(Whole::Outline), from), page, page] | ||
| 2178 | ); | ||
| 2179 | } | ||
| 2180 | // The title has no object level: its text, then the page. | ||
| 2181 | let title = view.editor.outlines()[0].id; | ||
| 2182 | assert_eq!(levels(&mut view, title), [(None, title), page, page, page]); | ||
| 2183 | // The selection it chose ends with any other; an arrow leaves the caret at its start. | ||
| 2184 | let _ = view.key(&Key::Named(NamedKey::ArrowLeft), None).unwrap(); | ||
| 2185 | assert_eq!(view.editor.whole(), None); | ||
| 2186 | assert_eq!(view.editor.active_outline().id, delta); | ||
| 2187 | assert_eq!(view.editor.selection().positions, [START; 2]); | ||
| 2188 | } | ||
| 2189 | |||
| 2190 | /// Each outline Select All holds shows its frame and a grey body, its text highlighted. | ||
| 2191 | #[test] | ||
| 2192 | fn a_page_selection_frames_and_greys_every_body_outline() { | ||
| 2193 | let (mut view, [alpha, ..]) = page_selection_view(); | ||
| 2194 | let grey = crate::gpu::colorref(0x00f0f0f0); | ||
| 2195 | let paint = |view: &PageView| { | ||
| 2196 | let primitives = view.primitives(COLORS).unwrap(); | ||
| 2197 | let count = | ||
| 2198 | |wanted: &dyn Fn(&Primitive) -> bool| primitives.iter().filter(|p| wanted(p)).count(); | ||
| 2199 | ( | ||
| 2200 | count( | ||
| 2201 | &|p| matches!(p, Primitive::RoundedRect { color, stroke: None, .. } if *color == grey), | ||
| 2202 | ), | ||
| 2203 | count(&|p| matches!(p, Primitive::Rect { color, .. } if *color == COLORS.selection)), | ||
| 2204 | ) | ||
| 2205 | }; | ||
| 2206 | view.editor.focus_outline(alpha).unwrap(); | ||
| 2207 | assert_eq!(paint(&view).0, 0); | ||
| 2208 | let _ = view.widen_selection().unwrap(); | ||
| 2209 | let _ = view.widen_selection().unwrap(); | ||
| 2210 | let (greyed, alpha) = paint(&view); | ||
| 2211 | assert_eq!(greyed, 1); | ||
| 2212 | let _ = view.widen_selection().unwrap(); | ||
| 2213 | // One line each of the others besides Alpha's. | ||
| 2214 | assert_eq!(paint(&view), (4, alpha + 3)); | ||
| 2215 | } | ||
| 2216 | |||
| 2217 | /// Keys on the page selection (lab): arrows up and down and Tab leave it, Enter and Right | ||
| 2218 | /// put the caret at the last outline's end, Delete and Backspace remove every outline and | ||
| 2219 | /// leave the caret where the first began, and typing replaces them all with the last outline | ||
| 2220 | /// holding the text. Each edit stores as one edit and undoes in one step to the selection. | ||
| 2221 | #[test] | ||
| 2222 | fn keys_act_on_the_page_selection_as_onenote_does() { | ||
| 2223 | let (mut view, [alpha, beta, gamma, delta]) = page_selection_view(); | ||
| 2224 | let end = TextPosition { | ||
| 2225 | paragraph: 0, | ||
| 2226 | offset: 5, | ||
| 2227 | }; | ||
| 2228 | for key in [ | ||
| 2229 | NamedKey::ArrowUp, | ||
| 2230 | NamedKey::ArrowDown, | ||
| 2231 | NamedKey::Tab, | ||
| 2232 | NamedKey::Enter, | ||
| 2233 | ] { | ||
| 2234 | select_page(&mut view, beta); | ||
| 2235 | let _ = view.key(&Key::Named(key), None).unwrap(); | ||
| 2236 | let kept = key != NamedKey::Enter; | ||
| 2237 | assert_eq!(view.editor.whole() == Some(Whole::Page), kept, "{key:?}"); | ||
| 2238 | if !kept { | ||
| 2239 | assert_eq!(view.editor.selection().positions, [end; 2]); | ||
| 2240 | } | ||
| 2241 | } | ||
| 2242 | assert_eq!(view.editor.take_ops().unwrap(), []); | ||
| 2243 | |||
| 2244 | let stored = origins(&view); | ||
| 2245 | for key in [NamedKey::Delete, NamedKey::Backspace] { | ||
| 2246 | select_page(&mut view, gamma); | ||
| 2247 | let _ = view.key(&Key::Named(key), None).unwrap(); | ||
| 2248 | assert!(view.editor.outlines().iter().all(|outline| outline.title)); | ||
| 2249 | assert_eq!( | ||
| 2250 | view.editor.caret_outline().map(TextOutline::origin), | ||
| 2251 | Some(stored[0]) | ||
| 2252 | ); | ||
| 2253 | let ops = view.editor.take_ops().unwrap(); | ||
| 2254 | let deleted = ops.iter().filter_map(|op| match op { | ||
| 2255 | onestore::op::PageOp::Delete { object } => Some(*object), | ||
| 2256 | _ => None, | ||
| 2257 | }); | ||
| 2258 | assert_eq!( | ||
| 2259 | deleted.collect::<BTreeSet<_>>(), | ||
| 2260 | BTreeSet::from([alpha, beta, gamma, delta]) | ||
| 2261 | ); | ||
| 2262 | assert_eq!(ops.len(), 4); | ||
| 2263 | let _ = view.undo(false).unwrap(); | ||
| 2264 | assert_eq!(origins(&view), stored); | ||
| 2265 | assert_eq!(view.editor.whole(), Some(Whole::Page)); | ||
| 2266 | view.editor.take_ops().unwrap(); | ||
| 2267 | } | ||
| 2268 | |||
| 2269 | select_page(&mut view, alpha); | ||
| 2270 | let _ = view.key(&Key::Character("Z".into()), Some("Z")).unwrap(); | ||
| 2271 | let left = view | ||
| 2272 | .editor | ||
| 2273 | .outlines() | ||
| 2274 | .iter() | ||
| 2275 | .filter(|outline| !outline.title); | ||
| 2276 | assert_eq!( | ||
| 2277 | left.map(|outline| (outline.id, outline.shown_text())) | ||
| 2278 | .collect::<Vec<_>>(), | ||
| 2279 | [(delta, "Z".to_owned())] | ||
| 2280 | ); | ||
| 2281 | let _ = view.undo(false).unwrap(); | ||
| 2282 | assert_eq!(origins(&view), stored); | ||
| 2283 | assert_eq!(view.editor.whole(), Some(Whole::Page)); | ||
| 2284 | assert_eq!(view.editor.active_outline().shown_text(), "Delta"); | ||
| 2285 | } | ||
| 2286 | |||
| 2287 | /// Copy takes the outlines top to bottom with a blank line between, as OneNote's plain text | ||
| 2288 | /// does; Cut removes them as Delete does. | ||
| 2289 | #[test] | ||
| 2290 | fn copy_and_cut_take_the_page_selection() { | ||
| 2291 | let (mut view, [alpha, ..]) = page_selection_view(); | ||
| 2292 | select_page(&mut view, alpha); | ||
| 2293 | let copied = Some(Request::Copy( | ||
| 2294 | "Delta\n\nAlpha one\nAlpha two\n\nBeta\n\nGamma long line of text".into(), | ||
| 2295 | )); | ||
| 2296 | assert_eq!(view.copy(false).unwrap().request, copied); | ||
| 2297 | assert_eq!(view.editor.whole(), Some(Whole::Page)); | ||
| 2298 | assert_eq!(view.copy(true).unwrap().request, copied); | ||
| 2299 | assert!(view.editor.outlines().iter().all(|outline| outline.title)); | ||
| 2300 | assert!(view.editor.caret_outline().is_some()); | ||
| 2301 | } | ||
| 2302 | |||
| 2303 | /// Dragging any selected outline's handle moves them all by one snapped offset, stored as one | ||
| 2304 | /// edit and undone in one step; a click on a handle selects that outline alone. | ||
| 2305 | #[test] | ||
| 2306 | fn a_dragged_page_selection_moves_every_outline() { | ||
| 2307 | let (mut view, [alpha, beta, ..]) = page_selection_view(); | ||
| 2308 | let stored = origins(&view); | ||
| 2309 | select_page(&mut view, alpha); | ||
| 2310 | let now = Instant::now(); | ||
| 2311 | let bounds = view | ||
| 2312 | .editor | ||
| 2313 | .outlines() | ||
| 2314 | .iter() | ||
| 2315 | .find(|outline| outline.id == beta) | ||
| 2316 | .unwrap() | ||
| 2317 | .bounds(); | ||
| 2318 | let header = [(bounds.x0 + bounds.x1) as f32 / 2.0, bounds.y0 as f32 - 8.0]; | ||
| 2319 | let _ = view.pointer_moved(header).unwrap(); | ||
| 2320 | let _ = view.pointer_pressed(now).unwrap(); | ||
| 2321 | assert_eq!(view.editor.whole(), Some(Whole::Page)); | ||
| 2322 | let _ = view | ||
| 2323 | .pointer_moved([header[0] + 30.0, header[1] + 40.0]) | ||
| 2324 | .unwrap(); | ||
| 2325 | // Every selected outline follows the drag while it lasts. | ||
| 2326 | let dragged = view.primitives(COLORS).unwrap().len(); | ||
| 2327 | assert!(dragged > 0); | ||
| 2328 | let _ = view.pointer_released().unwrap(); | ||
| 2329 | // Beta lands on the grid, 18 pt cells from the margin origin, and the rest follow. | ||
| 2330 | let moved = origins(&view); | ||
| 2331 | assert_eq!(moved[1], [324.0, 212.4]); | ||
| 2332 | for (before, after) in stored.iter().zip(&moved) { | ||
| 2333 | for axis in 0..2 { | ||
| 2334 | let delta = moved[1][axis] - stored[1][axis]; | ||
| 2335 | assert!((after[axis] - before[axis] - delta).abs() < 1e-3); | ||
| 2336 | } | ||
| 2337 | } | ||
| 2338 | assert_eq!(view.editor.whole(), Some(Whole::Page)); | ||
| 2339 | assert_eq!(view.editor.take_ops().unwrap().len(), 4); | ||
| 2340 | let _ = view.undo(false).unwrap(); | ||
| 2341 | assert_eq!(origins(&view), stored); | ||
| 2342 | |||
| 2343 | // Option-Command-arrows nudge the whole selection. | ||
| 2344 | let _ = view | ||
| 2345 | .modifiers_changed(Modifiers { | ||
| 2346 | command: true, | ||
| 2347 | option: true, | ||
| 2348 | ..Modifiers::default() | ||
| 2349 | }) | ||
| 2350 | .unwrap(); | ||
| 2351 | let _ = view.key(&Key::Named(NamedKey::ArrowRight), None).unwrap(); | ||
| 2352 | let _ = view.modifiers_changed(Modifiers::default()).unwrap(); | ||
| 2353 | assert!( | ||
| 2354 | origins(&view) | ||
| 2355 | .iter() | ||
| 2356 | .zip(&stored) | ||
| 2357 | .all(|(after, before)| *after == [before[0] + 1.0, before[1]]) | ||
| 2358 | ); | ||
| 2359 | |||
| 2360 | let _ = view.pointer_moved(header).unwrap(); | ||
| 2361 | let _ = view.pointer_pressed(now + Duration::from_secs(1)).unwrap(); | ||
| 2362 | let _ = view.pointer_released().unwrap(); | ||
| 2363 | assert_eq!(view.editor.whole(), Some(Whole::Outline)); | ||
| 2364 | assert_eq!(view.editor.active_outline().id, beta); | ||
| 2365 | } | ||
| 2366 | |||
| 2367 | /// A toggle on the page selection turns on everywhere unless every outline has it, as one | ||
| 2368 | /// undo step that keeps the selection. | ||
| 2369 | #[test] | ||
| 2370 | fn formatting_the_page_selection_settles_on_one_state() { | ||
| 2371 | let (mut view, [alpha, beta, ..]) = page_selection_view(); | ||
| 2372 | let bold = |view: &mut PageView| { | ||
| 2373 | let ids = view | ||
| 2374 | .editor | ||
| 2375 | .outlines() | ||
| 2376 | .iter() | ||
| 2377 | .filter(|outline| !outline.title) | ||
| 2378 | .map(|outline| outline.id) | ||
| 2379 | .collect::<Vec<_>>(); | ||
| 2380 | let focus = view.editor.active_outline().id; | ||
| 2381 | let selection = view.editor.selection(); | ||
| 2382 | let whole = view.editor.whole(); | ||
| 2383 | let states = ids | ||
| 2384 | .into_iter() | ||
| 2385 | .map(|id| { | ||
| 2386 | view.editor.focus_outline(id).unwrap(); | ||
| 2387 | view.editor.select_all().unwrap(); | ||
| 2388 | view.editor | ||
| 2389 | .format_state() | ||
| 2390 | .unwrap() | ||
| 2391 | .toggles | ||
| 2392 | .contains(&Toggle::Bold) | ||
| 2393 | }) | ||
| 2394 | .collect::<Vec<_>>(); | ||
| 2395 | if whole == Some(Whole::Page) { | ||
| 2396 | select_page(view, focus); | ||
| 2397 | } else { | ||
| 2398 | view.editor.focus_outline(focus).unwrap(); | ||
| 2399 | view.editor.select(selection).unwrap(); | ||
| 2400 | } | ||
| 2401 | states | ||
| 2402 | }; | ||
| 2403 | view.editor.focus_outline(beta).unwrap(); | ||
| 2404 | view.editor.select_all().unwrap(); | ||
| 2405 | let _ = view.format(Formatting::Toggle(Toggle::Bold)).unwrap(); | ||
| 2406 | assert_eq!(bold(&mut view), [false, true, false, false]); | ||
| 2407 | select_page(&mut view, alpha); | ||
| 2408 | let _ = view.format(Formatting::Toggle(Toggle::Bold)).unwrap(); | ||
| 2409 | assert_eq!(view.editor.whole(), Some(Whole::Page)); | ||
| 2410 | assert_eq!(bold(&mut view), [true; 4]); | ||
| 2411 | let _ = view.format(Formatting::Toggle(Toggle::Bold)).unwrap(); | ||
| 2412 | assert_eq!(bold(&mut view), [false; 4]); | ||
| 2413 | let _ = view.undo(false).unwrap(); | ||
| 2414 | assert_eq!(bold(&mut view), [true; 4]); | ||
| 2415 | assert_eq!(view.editor.whole(), Some(Whole::Page)); | ||
| 2416 | } | ||
| 2417 | |||
| 2418 | /// Selecting the last outline's text again by other means selects only that text. | ||
| 2419 | #[test] | ||
| 2420 | fn the_page_selection_does_not_return_with_its_text_selection() { | ||
| 2421 | let (mut view, [alpha, ..]) = page_selection_view(); | ||
| 2422 | select_page(&mut view, alpha); | ||
| 2423 | let whole = view.editor.selection(); | ||
| 2424 | let _ = view.key(&Key::Named(NamedKey::ArrowRight), None).unwrap(); | ||
| 2425 | view.editor.select(whole).unwrap(); | ||
| 2426 | assert_eq!(view.editor.whole(), None); | ||
| 2427 | } |
crates/canvas/src/layout.rs+6-2| ... | @@ -102,6 +102,10 @@ impl fmt::Display for LayoutError { | ... | @@ -102,6 +102,10 @@ impl fmt::Display for LayoutError { |
| 102 | 102 | ||
| 103 | impl std::error::Error for LayoutError {} | 103 | impl std::error::Error for LayoutError {} |
| 104 | 104 | ||
| 105 | /// What a run without a font or size of its own is laid out in. | ||
| 106 | pub const DEFAULT_FONT: &str = "Arial"; | ||
| 107 | pub const DEFAULT_FONT_SIZE: f32 = 11.0; | ||
| 108 | |||
| 105 | /// Metric-compatible faces for the fonts OneNote pages use most, under the SIL Open Font | 109 | /// Metric-compatible faces for the fonts OneNote pages use most, under the SIL Open Font |
| 106 | /// Licence files beside them, by the family each stands in for. | 110 | /// Licence files beside them, by the family each stands in for. |
| 107 | const BUNDLED: [(&str, &[&[u8]]); 4] = [ | 111 | const BUNDLED: [(&str, &[&[u8]]); 4] = [ |
| ... | @@ -283,7 +287,7 @@ impl TextEngine { | ... | @@ -283,7 +287,7 @@ impl TextEngine { |
| 283 | for (index, span) in paragraph.spans().iter().enumerate() { | 287 | for (index, span) in paragraph.spans().iter().enumerate() { |
| 284 | let format = &span.format; | 288 | let format = &span.format; |
| 285 | let link = format.hyperlink == Some(true); | 289 | let link = format.hyperlink == Some(true); |
| 286 | let size = format.font_size.unwrap_or(11.0); | 290 | let size = format.font_size.unwrap_or(DEFAULT_FONT_SIZE); |
| 287 | if !size.is_finite() || size <= 0.0 { | 291 | if !size.is_finite() || size <= 0.0 { |
| 288 | return Err(LayoutError::InvalidFontSize); | 292 | return Err(LayoutError::InvalidFontSize); |
| 289 | } | 293 | } |
| ... | @@ -303,7 +307,7 @@ impl TextEngine { | ... | @@ -303,7 +307,7 @@ impl TextEngine { |
| 303 | StyleProperty::FontFamily(FontFamily::List( | 307 | StyleProperty::FontFamily(FontFamily::List( |
| 304 | [ | 308 | [ |
| 305 | Some(FontFamilyName::named( | 309 | Some(FontFamilyName::named( |
| 306 | format.font.as_deref().unwrap_or("Arial"), | 310 | format.font.as_deref().unwrap_or(DEFAULT_FONT), |
| 307 | )), | 311 | )), |
| 308 | // macOS ships STIX Two Math where Windows has Cambria Math. | 312 | // macOS ships STIX Two Math where Windows has Cambria Math. |
| 309 | (format.font.as_deref() == Some("Cambria Math")) | 313 | (format.font.as_deref() == Some("Cambria Math")) |
crates/canvas/src/math.rs+1-1| ... | @@ -612,7 +612,7 @@ pub(crate) fn built(paragraph: &Paragraph) -> Vec<std::ops::Range<usize>> { | ... | @@ -612,7 +612,7 @@ pub(crate) fn built(paragraph: &Paragraph) -> Vec<std::ops::Range<usize>> { |
| 612 | pub fn layout(engine: &mut TextEngine, paragraph: &Paragraph) -> Result<MathLayout, LayoutError> { | 612 | pub fn layout(engine: &mut TextEngine, paragraph: &Paragraph) -> Result<MathLayout, LayoutError> { |
| 613 | let nodes = Math::parse(paragraph).map_err(|_| LayoutError::UnsupportedContent)?; | 613 | let nodes = Math::parse(paragraph).map_err(|_| LayoutError::UnsupportedContent)?; |
| 614 | let format = &paragraph.spans()[0].format; | 614 | let format = &paragraph.spans()[0].format; |
| 615 | let size = format.font_size.unwrap_or(11.0); | 615 | let size = format.font_size.unwrap_or(crate::layout::DEFAULT_FONT_SIZE); |
| 616 | let color = format.color.filter(|color| *color != 0xff00_0000); | 616 | let color = format.color.filter(|color| *color != 0xff00_0000); |
| 617 | // An equation line is at least as tall as a line of text at its size. | 617 | // An equation line is at least as tall as a line of text at its size. |
| 618 | let strut = engine.layout( | 618 | let strut = engine.layout( |
crates/canvas/src/outline.rs+28-17| ... | @@ -231,6 +231,30 @@ pub enum TagIcon { | ... | @@ -231,6 +231,30 @@ pub enum TagIcon { |
| 231 | Other, | 231 | Other, |
| 232 | } | 232 | } |
| 233 | 233 | ||
| 234 | impl TagIcon { | ||
| 235 | /// How the page draws tag symbol `shape`, `checked` where it is a check box; none for | ||
| 236 | /// shape 0, which marks the text instead. | ||
| 237 | pub fn of(shape: u16, checked: bool) -> Option<Self> { | ||
| 238 | Some(match shape { | ||
| 239 | 0 => return None, | ||
| 240 | // OneNote's check boxes, plain and with a person, arrow, number or bulb. | ||
| 241 | 3 | 8 | 12 | 28 | 71 | 94 | 95 => Self::CheckBox { checked }, | ||
| 242 | 13 => Self::Star, | ||
| 243 | 15 => Self::Question, | ||
| 244 | 17 => Self::Exclamation, | ||
| 245 | 18 => Self::Phone, | ||
| 246 | 23 => Self::Address, | ||
| 247 | 118 => Self::Contact, | ||
| 248 | 136 => Self::Highlight, | ||
| 249 | 100 => Self::RedSquare, | ||
| 250 | 101 => Self::YellowSquare, | ||
| 251 | 102 => Self::BlueSquare, | ||
| 252 | 121 => Self::Music, | ||
| 253 | _ => Self::Other, | ||
| 254 | }) | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 234 | #[derive(Clone, Debug, PartialEq)] | 258 | #[derive(Clone, Debug, PartialEq)] |
| 235 | pub struct ParagraphTag { | 259 | pub struct ParagraphTag { |
| 236 | pub icon: TagIcon, | 260 | pub icon: TagIcon, |
| ... | @@ -823,25 +847,12 @@ impl ParagraphLayout { | ... | @@ -823,25 +847,12 @@ impl ParagraphLayout { |
| 823 | let (tag, shape, label) = (tag.0, tag.1, tag.2); | 847 | let (tag, shape, label) = (tag.0, tag.1, tag.2); |
| 824 | let icon = match shape { | 848 | let icon = match shape { |
| 825 | None => TagIcon::Flag, | 849 | None => TagIcon::Flag, |
| 826 | Some(Some(0) | None) => continue, | 850 | Some(shape) => match TagIcon::of(shape.unwrap_or(0), tag.status & 1 != 0) { |
| 827 | Some(Some(3)) => TagIcon::CheckBox { | 851 | Some(icon) => icon, |
| 828 | checked: tag.status & 1 != 0, | 852 | None => continue, |
| 829 | }, | 853 | }, |
| 830 | Some(Some(13)) => TagIcon::Star, | ||
| 831 | Some(Some(15)) => TagIcon::Question, | ||
| 832 | Some(Some(17)) => TagIcon::Exclamation, | ||
| 833 | Some(Some(18)) => TagIcon::Phone, | ||
| 834 | Some(Some(23)) => TagIcon::Address, | ||
| 835 | Some(Some(118)) => TagIcon::Contact, | ||
| 836 | Some(Some(136)) => TagIcon::Highlight, | ||
| 837 | Some(Some(100)) => TagIcon::RedSquare, | ||
| 838 | Some(Some(101)) => TagIcon::YellowSquare, | ||
| 839 | Some(Some(102)) => TagIcon::BlueSquare, | ||
| 840 | Some(Some(121)) => TagIcon::Music, | ||
| 841 | // A symbol without artwork here shows as a plain tag. | ||
| 842 | Some(_) => TagIcon::Other, | ||
| 843 | }; | 854 | }; |
| 844 | let size = format.font_size.unwrap_or(11.0); | 855 | let size = format.font_size.unwrap_or(crate::layout::DEFAULT_FONT_SIZE); |
| 845 | let side = ParagraphTag::side(size); | 856 | let side = ParagraphTag::side(size); |
| 846 | tags.push(ParagraphTag { | 857 | tags.push(ParagraphTag { |
| 847 | icon, | 858 | icon, |
crates/canvas/tests/ops_differential.rs+31-2| ... | @@ -7,7 +7,7 @@ | ... | @@ -7,7 +7,7 @@ |
| 7 | 7 | ||
| 8 | use canvas::{ | 8 | use canvas::{ |
| 9 | document::TextPosition, | 9 | document::TextPosition, |
| 10 | editor::{Alignment, CanvasEditor, Formatting, NoteTag, Toggle}, | 10 | editor::{Alignment, CanvasEditor, Formatting, NoteTag, Toggle, Whole}, |
| 11 | layout::TextEngine, | 11 | layout::TextEngine, |
| 12 | }; | 12 | }; |
| 13 | use draw::edit::Movement; | 13 | use draw::edit::Movement; |
| ... | @@ -91,11 +91,15 @@ enum Kind { | ... | @@ -91,11 +91,15 @@ enum Kind { |
| 91 | Equation, | 91 | Equation, |
| 92 | Linear, | 92 | Linear, |
| 93 | Professional, | 93 | Professional, |
| 94 | PageDelete, | ||
| 95 | PageType, | ||
| 96 | PageMove, | ||
| 97 | PageBold, | ||
| 94 | Undo, | 98 | Undo, |
| 95 | Redo, | 99 | Redo, |
| 96 | } | 100 | } |
| 97 | 101 | ||
| 98 | const KINDS: [Kind; 48] = [ | 102 | const KINDS: [Kind; 52] = [ |
| 99 | Kind::Type, | 103 | Kind::Type, |
| 100 | Kind::TypeEnd, | 104 | Kind::TypeEnd, |
| 101 | Kind::Compose, | 105 | Kind::Compose, |
| ... | @@ -142,6 +146,10 @@ const KINDS: [Kind; 48] = [ | ... | @@ -142,6 +146,10 @@ const KINDS: [Kind; 48] = [ |
| 142 | Kind::Equation, | 146 | Kind::Equation, |
| 143 | Kind::Linear, | 147 | Kind::Linear, |
| 144 | Kind::Professional, | 148 | Kind::Professional, |
| 149 | Kind::PageDelete, | ||
| 150 | Kind::PageType, | ||
| 151 | Kind::PageMove, | ||
| 152 | Kind::PageBold, | ||
| 145 | Kind::Undo, | 153 | Kind::Undo, |
| 146 | Kind::Redo, | 154 | Kind::Redo, |
| 147 | ]; | 155 | ]; |
| ... | @@ -253,6 +261,18 @@ fn perform( | ... | @@ -253,6 +261,18 @@ fn perform( |
| 253 | |editor: &mut CanvasEditor, engine: &mut TextEngine, random: &mut Random, command| { | 261 | |editor: &mut CanvasEditor, engine: &mut TextEngine, random: &mut Random, command| { |
| 254 | range(editor, random) && editor.format(engine, command).is_ok() | 262 | range(editor, random) && editor.format(engine, command).is_ok() |
| 255 | }; | 263 | }; |
| 264 | // Select All until it holds every body outline. | ||
| 265 | let page = |editor: &mut CanvasEditor| { | ||
| 266 | for _ in 0..16 { | ||
| 267 | if editor.whole() == Some(Whole::Page) { | ||
| 268 | return true; | ||
| 269 | } | ||
| 270 | if editor.widen_selection().is_err() { | ||
| 271 | return false; | ||
| 272 | } | ||
| 273 | } | ||
| 274 | false | ||
| 275 | }; | ||
| 256 | match kind { | 276 | match kind { |
| 257 | Kind::Type => place(editor, middle) && editor.insert(engine, "ab").is_ok(), | 277 | Kind::Type => place(editor, middle) && editor.insert(engine, "ab").is_ok(), |
| 258 | Kind::TypeEnd => place(editor, length) && editor.commit_text(engine, "z".into()).is_ok(), | 278 | Kind::TypeEnd => place(editor, length) && editor.commit_text(engine, "z".into()).is_ok(), |
| ... | @@ -409,6 +429,15 @@ fn perform( | ... | @@ -409,6 +429,15 @@ fn perform( |
| 409 | Kind::Professional => { | 429 | Kind::Professional => { |
| 410 | place(editor, middle) && editor.build_equation(engine).unwrap_or(false) | 430 | place(editor, middle) && editor.build_equation(engine).unwrap_or(false) |
| 411 | } | 431 | } |
| 432 | Kind::PageDelete => page(editor) && editor.delete(engine, false).is_ok(), | ||
| 433 | Kind::PageType => page(editor) && editor.insert(engine, "Z").is_ok(), | ||
| 434 | Kind::PageMove => page(editor) && editor.move_page([36.0, 18.0]).is_ok(), | ||
| 435 | Kind::PageBold => { | ||
| 436 | page(editor) | ||
| 437 | && editor | ||
| 438 | .format(engine, Formatting::Toggle(Toggle::Bold)) | ||
| 439 | .is_ok() | ||
| 440 | } | ||
| 412 | Kind::Undo => editor.undo(engine).unwrap_or(false), | 441 | Kind::Undo => editor.undo(engine).unwrap_or(false), |
| 413 | Kind::Redo => editor.redo(engine).unwrap_or(false), | 442 | Kind::Redo => editor.redo(engine).unwrap_or(false), |
| 414 | } | 443 | } |
crates/draw/Cargo.toml+5-3| ... | @@ -5,9 +5,11 @@ edition = "2024" | ... | @@ -5,9 +5,11 @@ edition = "2024" |
| 5 | publish = false | 5 | publish = false |
| 6 | 6 | ||
| 7 | [features] | 7 | [features] |
| 8 | default = ["render"] | 8 | default = ["wgpu"] |
| 9 | # The wgpu renderer; `edit` needs only parley. | 9 | # The renderer; `edit` needs only parley. It submits through wgpu, or without it through |
| 10 | render = ["dep:bytemuck", "dep:image", "dep:linebender_resource_handle", "dep:roxmltree", "dep:swash", "dep:wgpu"] | 10 | # OpenGL 2.1, for Mac OS X 10.6. |
| 11 | render = ["dep:bytemuck", "dep:image", "dep:linebender_resource_handle", "dep:roxmltree", "dep:swash"] | ||
| 12 | wgpu = ["render", "dep:wgpu"] | ||
| 11 | 13 | ||
| 12 | [dependencies] | 14 | [dependencies] |
| 13 | parley.workspace = true | 15 | parley.workspace = true |
crates/draw/src/draw.glsl created+102| ... | @@ -0,0 +1,102 @@ | ||
| 1 | // GLSL 1.20 port of crates/draw/src/draw.wgsl. GLSL 1.20 has no flat interpolation, so | ||
| 2 | // the per-quad shape and stroke arrive interpolated between equal values: equality tests | ||
| 3 | // on them use a tolerance. | ||
| 4 | #ifdef VERTEX | ||
| 5 | attribute vec2 position; | ||
| 6 | attribute vec2 uv; | ||
| 7 | attribute vec4 color; | ||
| 8 | attribute vec2 local; | ||
| 9 | attribute vec4 shape; | ||
| 10 | attribute float stroke; | ||
| 11 | #endif | ||
| 12 | varying vec2 v_uv; | ||
| 13 | varying vec4 v_color; | ||
| 14 | varying vec2 v_local; | ||
| 15 | varying vec4 v_shape; | ||
| 16 | varying float v_stroke; | ||
| 17 | |||
| 18 | #ifdef VERTEX | ||
| 19 | void main() { | ||
| 20 | v_uv = uv; | ||
| 21 | v_color = color; | ||
| 22 | v_local = local; | ||
| 23 | v_shape = shape; | ||
| 24 | v_stroke = stroke; | ||
| 25 | gl_Position = vec4(position, 0.0, 1.0); | ||
| 26 | } | ||
| 27 | #else | ||
| 28 | uniform sampler2D atlas; | ||
| 29 | |||
| 30 | bool same(float a, float b) { | ||
| 31 | return abs(a - b) <= 1e-4 * max(1.0, max(abs(a), abs(b))); | ||
| 32 | } | ||
| 33 | |||
| 34 | float ellipse_arc(float angle, vec2 radius) { | ||
| 35 | if (same(radius.x, radius.y)) { | ||
| 36 | return angle * radius.x; | ||
| 37 | } | ||
| 38 | // Four-point Gauss-Legendre quadrature. | ||
| 39 | vec4 nodes = vec4(-0.86113631, -0.33998104, 0.33998104, 0.86113631); | ||
| 40 | vec4 weights = vec4(0.34785485, 0.65214515, 0.65214515, 0.34785485); | ||
| 41 | float half_angle = angle * 0.5; | ||
| 42 | float scale = max(radius.x, radius.y); | ||
| 43 | float sum = 0.0; | ||
| 44 | for (int i = 0; i < 4; i++) { | ||
| 45 | float t = half_angle * (nodes[i] + 1.0); | ||
| 46 | sum += weights[i] * length((radius / scale) * vec2(sin(t), cos(t))); | ||
| 47 | } | ||
| 48 | return half_angle * sum * scale; | ||
| 49 | } | ||
| 50 | |||
| 51 | void main() { | ||
| 52 | vec4 color = texture2D(atlas, v_uv) * v_color; | ||
| 53 | vec4 shape = v_shape; | ||
| 54 | if (shape.x > 0.0 && shape.y > 0.0) { | ||
| 55 | vec2 q = abs(v_local) - shape.xy + shape.zw; | ||
| 56 | float distance; | ||
| 57 | if (same(shape.z, shape.w)) { | ||
| 58 | distance = length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - shape.z; | ||
| 59 | } else if (all(greaterThan(q, vec2(0.0)))) { | ||
| 60 | vec2 radius = max(shape.zw, vec2(0.0001)); | ||
| 61 | vec2 normalized = q / radius; | ||
| 62 | float k0 = length(normalized); | ||
| 63 | distance = k0 * (k0 - 1.0) / max(length(normalized / radius), 1e-20); | ||
| 64 | } else { | ||
| 65 | vec2 edge = abs(v_local) - shape.xy; | ||
| 66 | distance = max(edge.x, edge.y); | ||
| 67 | } | ||
| 68 | float coverage = clamp(0.5 - distance, 0.0, 1.0); | ||
| 69 | float width = abs(v_stroke); | ||
| 70 | if (width > 1e-6) { | ||
| 71 | coverage *= clamp(distance + width + 0.5, 0.0, 1.0); | ||
| 72 | } | ||
| 73 | if (v_stroke < -1e-6) { | ||
| 74 | vec2 radius = max(shape.zw - width * 0.5, vec2(0.0)); | ||
| 75 | if (radius.x <= 1e-6 || radius.y <= 1e-6) { | ||
| 76 | radius = vec2(0.0); | ||
| 77 | } | ||
| 78 | vec2 straight = shape.xy - width * 0.5 - radius; | ||
| 79 | vec2 p = abs(v_local); | ||
| 80 | float quarter = straight.x + straight.y + ellipse_arc(1.57079632679, radius); | ||
| 81 | float along; | ||
| 82 | if (p.y <= straight.y) { | ||
| 83 | along = p.y; | ||
| 84 | } else if (p.x <= straight.x || radius.x == 0.0) { | ||
| 85 | along = quarter - p.x; | ||
| 86 | } else { | ||
| 87 | float angle = atan((p.y - straight.y) / radius.y, (p.x - straight.x) / radius.x); | ||
| 88 | along = straight.y + ellipse_arc(angle, radius); | ||
| 89 | } | ||
| 90 | if (v_local.x < 0.0) { | ||
| 91 | along = v_local.y < 0.0 ? 2.0 * quarter + along : 2.0 * quarter - along; | ||
| 92 | } else if (v_local.y < 0.0) { | ||
| 93 | along = 4.0 * quarter - along; | ||
| 94 | } | ||
| 95 | float phase = fract(along / (4.0 * width)) * (4.0 * width); | ||
| 96 | coverage *= clamp(width + 0.5 - abs(phase - 2.0 * width), 0.0, 1.0); | ||
| 97 | } | ||
| 98 | color *= coverage; | ||
| 99 | } | ||
| 100 | gl_FragColor = color; | ||
| 101 | } | ||
| 102 | #endif | ||
crates/draw/src/edit.rs+1| ... | @@ -32,6 +32,7 @@ pub enum NamedKey { | ... | @@ -32,6 +32,7 @@ pub enum NamedKey { |
| 32 | End, | 32 | End, |
| 33 | PageUp, | 33 | PageUp, |
| 34 | PageDown, | 34 | PageDown, |
| 35 | F11, | ||
| 35 | /// Shift, Control, Option or Command pressed alone. | 36 | /// Shift, Control, Option or Command pressed alone. |
| 36 | Modifier, | 37 | Modifier, |
| 37 | Other, | 38 | Other, |
crates/draw/src/lib.rs+2-2| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | //! What the page and the interface share: painting layers of glyphs, icons, paths, | 1 | //! What the page and the interface share: painting layers of glyphs, icons, paths, |
| 2 | //! images, rounded rectangles and pen strokes with wgpu, each layer with its own transform | 2 | //! images, rounded rectangles and pen strokes with wgpu or OpenGL, each layer with its |
| 3 | //! and clip, and editing text within a laid-out line. | 3 | //! own transform and clip, and editing text within a laid-out line. |
| 4 | 4 | ||
| 5 | pub mod edit; | 5 | pub mod edit; |
| 6 | #[cfg(feature = "render")] | 6 | #[cfg(feature = "render")] |
crates/draw/src/render.rs+88-276| ... | @@ -1,10 +1,21 @@ | ... | @@ -1,10 +1,21 @@ |
| 1 | #[cfg(not(feature = "wgpu"))] | ||
| 2 | mod gl; | ||
| 1 | mod icon; | 3 | mod icon; |
| 2 | |||
| 3 | pub use icon::Palette; | ||
| 4 | mod text; | 4 | mod text; |
| 5 | #[cfg(feature = "wgpu")] | ||
| 5 | mod translucent; | 6 | mod translucent; |
| 7 | #[cfg(feature = "wgpu")] | ||
| 8 | mod webgpu; | ||
| 6 | 9 | ||
| 10 | #[cfg(not(feature = "wgpu"))] | ||
| 11 | use gl as backend; | ||
| 12 | #[cfg(feature = "wgpu")] | ||
| 13 | use webgpu as backend; | ||
| 14 | |||
| 15 | pub use backend::Target; | ||
| 16 | pub use icon::Palette; | ||
| 7 | pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run}; | 17 | pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run}; |
| 18 | #[cfg(feature = "wgpu")] | ||
| 8 | pub use translucent::Translucent; | 19 | pub use translucent::Translucent; |
| 9 | 20 | ||
| 10 | use bytemuck::{Pod, Zeroable}; | 21 | use bytemuck::{Pod, Zeroable}; |
| ... | @@ -24,6 +35,7 @@ use swash::{ | ... | @@ -24,6 +35,7 @@ use swash::{ |
| 24 | const ATLAS_SIZE: u32 = 2048; | 35 | const ATLAS_SIZE: u32 = 2048; |
| 25 | const MAX_GLYPHS: usize = 8192; | 36 | const MAX_GLYPHS: usize = 8192; |
| 26 | const MAX_VERTICES: usize = 65_536; | 37 | const MAX_VERTICES: usize = 65_536; |
| 38 | const VERTEX_BUFFER_BYTES: u64 = (MAX_VERTICES * size_of::<Vertex>()) as u64; | ||
| 27 | /// Decoded bytes of all images one frame may paint. | 39 | /// Decoded bytes of all images one frame may paint. |
| 28 | pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024; | 40 | pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024; |
| 29 | const MAX_IMAGES: usize = 256; | 41 | const MAX_IMAGES: usize = 256; |
| ... | @@ -146,7 +158,7 @@ impl RasterImage { | ... | @@ -146,7 +158,7 @@ impl RasterImage { |
| 146 | } | 158 | } |
| 147 | 159 | ||
| 148 | struct CachedImage { | 160 | struct CachedImage { |
| 149 | binding: wgpu::BindGroup, | 161 | texture: backend::Image, |
| 150 | bytes: u64, | 162 | bytes: u64, |
| 151 | /// The texture goes when every copy of its image has. | 163 | /// The texture goes when every copy of its image has. |
| 152 | pixels: WeakBlob<u8>, | 164 | pixels: WeakBlob<u8>, |
| ... | @@ -190,6 +202,8 @@ enum AtlasKey { | ... | @@ -190,6 +202,8 @@ enum AtlasKey { |
| 190 | phase: [u8; 2], | 202 | phase: [u8; 2], |
| 191 | embolden: bool, | 203 | embolden: bool, |
| 192 | skew: u32, | 204 | skew: u32, |
| 205 | /// The ink's sRGB luminance in sixteenths, which `text_coverage` weights by. | ||
| 206 | tone: u8, | ||
| 193 | }, | 207 | }, |
| 194 | Icon { | 208 | Icon { |
| 195 | sources: &'static [&'static str], | 209 | sources: &'static [&'static str], |
| ... | @@ -435,15 +449,11 @@ pub struct Occupancy { | ... | @@ -435,15 +449,11 @@ pub struct Occupancy { |
| 435 | } | 449 | } |
| 436 | 450 | ||
| 437 | pub struct Renderer { | 451 | pub struct Renderer { |
| 452 | #[cfg(feature = "wgpu")] | ||
| 438 | pub device: wgpu::Device, | 453 | pub device: wgpu::Device, |
| 454 | #[cfg(feature = "wgpu")] | ||
| 439 | pub queue: wgpu::Queue, | 455 | pub queue: wgpu::Queue, |
| 440 | pipeline: wgpu::RenderPipeline, | 456 | gpu: backend::Gpu, |
| 441 | image_pipeline: wgpu::RenderPipeline, | ||
| 442 | erase_pipeline: wgpu::RenderPipeline, | ||
| 443 | atlas: wgpu::Texture, | ||
| 444 | bind_group: wgpu::BindGroup, | ||
| 445 | image_sampler: wgpu::Sampler, | ||
| 446 | vertex_buffer: wgpu::Buffer, | ||
| 447 | vertices: Vec<Vertex>, | 457 | vertices: Vec<Vertex>, |
| 448 | glyphs: HashMap<AtlasKey, Option<AtlasGlyph>>, | 458 | glyphs: HashMap<AtlasKey, Option<AtlasGlyph>>, |
| 449 | images: HashMap<u64, CachedImage>, | 459 | images: HashMap<u64, CachedImage>, |
| ... | @@ -454,150 +464,17 @@ pub struct Renderer { | ... | @@ -454,150 +464,17 @@ pub struct Renderer { |
| 454 | } | 464 | } |
| 455 | 465 | ||
| 456 | impl Renderer { | 466 | impl Renderer { |
| 457 | pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat) -> Self { | 467 | fn with_gpu( |
| 458 | let shader = device.create_shader_module(wgpu::include_wgsl!("draw.wgsl")); | 468 | #[cfg(feature = "wgpu")] device: wgpu::Device, |
| 459 | let binding_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { | 469 | #[cfg(feature = "wgpu")] queue: wgpu::Queue, |
| 460 | label: Some("Draw texture"), | 470 | gpu: backend::Gpu, |
| 461 | entries: &[ | 471 | ) -> Self { |
| 462 | wgpu::BindGroupLayoutEntry { | 472 | let renderer = Self { |
| 463 | binding: 0, | 473 | #[cfg(feature = "wgpu")] |
| 464 | visibility: wgpu::ShaderStages::FRAGMENT, | ||
| 465 | ty: wgpu::BindingType::Texture { | ||
| 466 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, | ||
| 467 | view_dimension: wgpu::TextureViewDimension::D2, | ||
| 468 | multisampled: false, | ||
| 469 | }, | ||
| 470 | count: None, | ||
| 471 | }, | ||
| 472 | wgpu::BindGroupLayoutEntry { | ||
| 473 | binding: 1, | ||
| 474 | visibility: wgpu::ShaderStages::FRAGMENT, | ||
| 475 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), | ||
| 476 | count: None, | ||
| 477 | }, | ||
| 478 | ], | ||
| 479 | }); | ||
| 480 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { | ||
| 481 | label: Some("Draw"), | ||
| 482 | bind_group_layouts: &[Some(&binding_layout)], | ||
| 483 | ..Default::default() | ||
| 484 | }); | ||
| 485 | let make_pipeline = |blend| { | ||
| 486 | device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { | ||
| 487 | label: Some("Draw"), | ||
| 488 | layout: Some(&pipeline_layout), | ||
| 489 | vertex: wgpu::VertexState { | ||
| 490 | module: &shader, | ||
| 491 | entry_point: Some("vertex"), | ||
| 492 | compilation_options: Default::default(), | ||
| 493 | buffers: &[Some(wgpu::VertexBufferLayout { | ||
| 494 | array_stride: size_of::<Vertex>() as u64, | ||
| 495 | step_mode: wgpu::VertexStepMode::Vertex, | ||
| 496 | attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32], | ||
| 497 | })], | ||
| 498 | }, | ||
| 499 | fragment: Some(wgpu::FragmentState { | ||
| 500 | module: &shader, | ||
| 501 | entry_point: Some("fragment"), | ||
| 502 | compilation_options: Default::default(), | ||
| 503 | targets: &[Some(wgpu::ColorTargetState { | ||
| 504 | format, | ||
| 505 | blend: Some(blend), | ||
| 506 | write_mask: wgpu::ColorWrites::ALL, | ||
| 507 | })], | ||
| 508 | }), | ||
| 509 | primitive: Default::default(), | ||
| 510 | depth_stencil: None, | ||
| 511 | multisample: Default::default(), | ||
| 512 | multiview_mask: None, | ||
| 513 | cache: None, | ||
| 514 | }) | ||
| 515 | }; | ||
| 516 | // Coverage accumulates in alpha, leaving premultiplied colour over a transparent clear. | ||
| 517 | let pipeline = make_pipeline(wgpu::BlendState { | ||
| 518 | color: wgpu::BlendState::ALPHA_BLENDING.color, | ||
| 519 | alpha: wgpu::BlendComponent::OVER, | ||
| 520 | }); | ||
| 521 | let image_pipeline = make_pipeline(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING); | ||
| 522 | let erase = wgpu::BlendComponent { | ||
| 523 | src_factor: wgpu::BlendFactor::Zero, | ||
| 524 | dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, | ||
| 525 | operation: wgpu::BlendOperation::Add, | ||
| 526 | }; | ||
| 527 | let erase_pipeline = make_pipeline(wgpu::BlendState { | ||
| 528 | color: erase, | ||
| 529 | alpha: erase, | ||
| 530 | }); | ||
| 531 | let atlas = device.create_texture(&wgpu::TextureDescriptor { | ||
| 532 | label: Some("Glyph atlas"), | ||
| 533 | size: wgpu::Extent3d { | ||
| 534 | width: ATLAS_SIZE, | ||
| 535 | height: ATLAS_SIZE, | ||
| 536 | depth_or_array_layers: 1, | ||
| 537 | }, | ||
| 538 | mip_level_count: 1, | ||
| 539 | sample_count: 1, | ||
| 540 | dimension: wgpu::TextureDimension::D2, | ||
| 541 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | ||
| 542 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | ||
| 543 | view_formats: &[], | ||
| 544 | }); | ||
| 545 | let view = atlas.create_view(&Default::default()); | ||
| 546 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { | ||
| 547 | mag_filter: wgpu::FilterMode::Nearest, | ||
| 548 | min_filter: wgpu::FilterMode::Nearest, | ||
| 549 | ..Default::default() | ||
| 550 | }); | ||
| 551 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { | ||
| 552 | label: Some("Glyph atlas"), | ||
| 553 | layout: &pipeline.get_bind_group_layout(0), | ||
| 554 | entries: &[ | ||
| 555 | wgpu::BindGroupEntry { | ||
| 556 | binding: 0, | ||
| 557 | resource: wgpu::BindingResource::TextureView(&view), | ||
| 558 | }, | ||
| 559 | wgpu::BindGroupEntry { | ||
| 560 | binding: 1, | ||
| 561 | resource: wgpu::BindingResource::Sampler(&sampler), | ||
| 562 | }, | ||
| 563 | ], | ||
| 564 | }); | ||
| 565 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { | ||
| 566 | label: Some("Draw vertices"), | ||
| 567 | size: (MAX_VERTICES * size_of::<Vertex>()) as u64, | ||
| 568 | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, | ||
| 569 | mapped_at_creation: false, | ||
| 570 | }); | ||
| 571 | let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor { | ||
| 572 | label: Some("Draw image filtering"), | ||
| 573 | mag_filter: wgpu::FilterMode::Linear, | ||
| 574 | min_filter: wgpu::FilterMode::Linear, | ||
| 575 | ..Default::default() | ||
| 576 | }); | ||
| 577 | queue.write_texture( | ||
| 578 | atlas.as_image_copy(), | ||
| 579 | &[255; 4], | ||
| 580 | wgpu::TexelCopyBufferLayout { | ||
| 581 | offset: 0, | ||
| 582 | bytes_per_row: Some(4), | ||
| 583 | rows_per_image: Some(1), | ||
| 584 | }, | ||
| 585 | wgpu::Extent3d { | ||
| 586 | width: 1, | ||
| 587 | height: 1, | ||
| 588 | depth_or_array_layers: 1, | ||
| 589 | }, | ||
| 590 | ); | ||
| 591 | Self { | ||
| 592 | device, | 474 | device, |
| 475 | #[cfg(feature = "wgpu")] | ||
| 593 | queue, | 476 | queue, |
| 594 | pipeline, | 477 | gpu, |
| 595 | image_pipeline, | ||
| 596 | erase_pipeline, | ||
| 597 | atlas, | ||
| 598 | bind_group, | ||
| 599 | image_sampler, | ||
| 600 | vertex_buffer, | ||
| 601 | vertices: Vec::new(), | 478 | vertices: Vec::new(), |
| 602 | glyphs: HashMap::new(), | 479 | glyphs: HashMap::new(), |
| 603 | images: HashMap::new(), | 480 | images: HashMap::new(), |
| ... | @@ -605,7 +482,10 @@ impl Renderer { | ... | @@ -605,7 +482,10 @@ impl Renderer { |
| 605 | scaler: ScaleContext::with_max_entries(32), | 482 | scaler: ScaleContext::with_max_entries(32), |
| 606 | pen: [1, 0], | 483 | pen: [1, 0], |
| 607 | row_height: 1, | 484 | row_height: 1, |
| 608 | } | 485 | }; |
| 486 | // The atlas's first texel is the white that solid fills sample. | ||
| 487 | renderer.write_atlas([0, 0], [1, 1], &[255; 4]); | ||
| 488 | renderer | ||
| 609 | } | 489 | } |
| 610 | 490 | ||
| 611 | pub fn clear_glyph_cache(&mut self) { | 491 | pub fn clear_glyph_cache(&mut self) { |
| ... | @@ -637,8 +517,8 @@ impl Renderer { | ... | @@ -637,8 +517,8 @@ impl Renderer { |
| 637 | batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(), | 517 | batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(), |
| 638 | glyph_capacity: self.glyphs.capacity(), | 518 | glyph_capacity: self.glyphs.capacity(), |
| 639 | image_capacity: self.images.capacity(), | 519 | image_capacity: self.images.capacity(), |
| 640 | atlas_bytes: u64::from(self.atlas.width()) * u64::from(self.atlas.height()) * 4, | 520 | atlas_bytes: u64::from(ATLAS_SIZE) * u64::from(ATLAS_SIZE) * 4, |
| 641 | vertex_buffer_bytes: self.vertex_buffer.size(), | 521 | vertex_buffer_bytes: VERTEX_BUFFER_BYTES, |
| 642 | within_budget: self.glyphs.len() <= MAX_GLYPHS | 522 | within_budget: self.glyphs.len() <= MAX_GLYPHS |
| 643 | && self.images.len() <= MAX_IMAGES | 523 | && self.images.len() <= MAX_IMAGES |
| 644 | && image_bytes <= MAX_IMAGE_BYTES | 524 | && image_bytes <= MAX_IMAGE_BYTES |
| ... | @@ -649,7 +529,7 @@ impl Renderer { | ... | @@ -649,7 +529,7 @@ impl Renderer { |
| 649 | /// Clears `target`, `size` device pixels, to linear `clear` and paints the layers in order. | 529 | /// Clears `target`, `size` device pixels, to linear `clear` and paints the layers in order. |
| 650 | pub fn draw( | 530 | pub fn draw( |
| 651 | &mut self, | 531 | &mut self, |
| 652 | target: &wgpu::TextureView, | 532 | target: &Target, |
| 653 | size: [u32; 2], | 533 | size: [u32; 2], |
| 654 | clear: [f32; 4], | 534 | clear: [f32; 4], |
| 655 | layers: &[Layer<'_>], | 535 | layers: &[Layer<'_>], |
| ... | @@ -702,39 +582,7 @@ impl Renderer { | ... | @@ -702,39 +582,7 @@ impl Renderer { |
| 702 | } | 582 | } |
| 703 | } | 583 | } |
| 704 | } | 584 | } |
| 705 | self.queue | 585 | self.submit(target, clear); |
| 706 | .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices)); | ||
| 707 | let mut encoder = self.device.create_command_encoder(&Default::default()); | ||
| 708 | { | ||
| 709 | let [r, g, b, a] = clear.map(f64::from); | ||
| 710 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { | ||
| 711 | label: Some("Draw"), | ||
| 712 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { | ||
| 713 | view: target, | ||
| 714 | depth_slice: None, | ||
| 715 | resolve_target: None, | ||
| 716 | ops: wgpu::Operations { | ||
| 717 | load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a }), | ||
| 718 | store: wgpu::StoreOp::Store, | ||
| 719 | }, | ||
| 720 | })], | ||
| 721 | ..Default::default() | ||
| 722 | }); | ||
| 723 | pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); | ||
| 724 | for batch in &self.batches { | ||
| 725 | let [x, y, width, height] = batch.scissor; | ||
| 726 | pass.set_scissor_rect(x, y, width, height); | ||
| 727 | let (pipeline, binding) = match batch.blend { | ||
| 728 | Blend::Over => (&self.pipeline, &self.bind_group), | ||
| 729 | Blend::Image(id) => (&self.image_pipeline, &self.images[&id].binding), | ||
| 730 | Blend::Erase => (&self.erase_pipeline, &self.bind_group), | ||
| 731 | }; | ||
| 732 | pass.set_pipeline(pipeline); | ||
| 733 | pass.set_bind_group(0, binding, &[]); | ||
| 734 | pass.draw(batch.vertices.clone(), 0..1); | ||
| 735 | } | ||
| 736 | } | ||
| 737 | self.queue.submit([encoder.finish()]); | ||
| 738 | Ok(()) | 586 | Ok(()) |
| 739 | } | 587 | } |
| 740 | 588 | ||
| ... | @@ -892,11 +740,7 @@ impl Renderer { | ... | @@ -892,11 +740,7 @@ impl Renderer { |
| 892 | if self.images.contains_key(&image.id()) { | 740 | if self.images.contains_key(&image.id()) { |
| 893 | return Ok(()); | 741 | return Ok(()); |
| 894 | } | 742 | } |
| 895 | if image | 743 | if image.size.iter().any(|v| *v > self.max_texture_dimension()) { |
| 896 | .size | ||
| 897 | .iter() | ||
| 898 | .any(|v| *v > self.device.limits().max_texture_dimension_2d) | ||
| 899 | { | ||
| 900 | return Err(RenderError::ImageTooLarge); | 744 | return Err(RenderError::ImageTooLarge); |
| 901 | } | 745 | } |
| 902 | let bytes = image.pixels().len() as u64; | 746 | let bytes = image.pixels().len() as u64; |
| ... | @@ -911,51 +755,10 @@ impl Renderer { | ... | @@ -911,51 +755,10 @@ impl Renderer { |
| 911 | .ok_or(RenderError::ImageBudget)?; | 755 | .ok_or(RenderError::ImageBudget)?; |
| 912 | self.images.remove(&id); | 756 | self.images.remove(&id); |
| 913 | } | 757 | } |
| 914 | let size = wgpu::Extent3d { | ||
| 915 | width: image.size[0], | ||
| 916 | height: image.size[1], | ||
| 917 | depth_or_array_layers: 1, | ||
| 918 | }; | ||
| 919 | let texture = self.device.create_texture(&wgpu::TextureDescriptor { | ||
| 920 | label: Some("Draw image"), | ||
| 921 | size, | ||
| 922 | mip_level_count: 1, | ||
| 923 | sample_count: 1, | ||
| 924 | dimension: wgpu::TextureDimension::D2, | ||
| 925 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | ||
| 926 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | ||
| 927 | view_formats: &[], | ||
| 928 | }); | ||
| 929 | self.queue.write_texture( | ||
| 930 | texture.as_image_copy(), | ||
| 931 | image.pixels(), | ||
| 932 | wgpu::TexelCopyBufferLayout { | ||
| 933 | offset: 0, | ||
| 934 | bytes_per_row: Some(image.size[0] * 4), | ||
| 935 | rows_per_image: Some(image.size[1]), | ||
| 936 | }, | ||
| 937 | size, | ||
| 938 | ); | ||
| 939 | let binding = self.device.create_bind_group(&wgpu::BindGroupDescriptor { | ||
| 940 | label: Some("Draw image"), | ||
| 941 | layout: &self.pipeline.get_bind_group_layout(0), | ||
| 942 | entries: &[ | ||
| 943 | wgpu::BindGroupEntry { | ||
| 944 | binding: 0, | ||
| 945 | resource: wgpu::BindingResource::TextureView( | ||
| 946 | &texture.create_view(&Default::default()), | ||
| 947 | ), | ||
| 948 | }, | ||
| 949 | wgpu::BindGroupEntry { | ||
| 950 | binding: 1, | ||
| 951 | resource: wgpu::BindingResource::Sampler(&self.image_sampler), | ||
| 952 | }, | ||
| 953 | ], | ||
| 954 | }); | ||
| 955 | self.images.insert( | 758 | self.images.insert( |
| 956 | image.id(), | 759 | image.id(), |
| 957 | CachedImage { | 760 | CachedImage { |
| 958 | binding, | 761 | texture: self.upload_image(image), |
| 959 | bytes, | 762 | bytes, |
| 960 | pixels: image.pixels.downgrade(), | 763 | pixels: image.pixels.downgrade(), |
| 961 | }, | 764 | }, |
| ... | @@ -980,6 +783,12 @@ impl Renderer { | ... | @@ -980,6 +783,12 @@ impl Renderer { |
| 980 | if !size.is_finite() || size > ATLAS_SIZE as f32 { | 783 | if !size.is_finite() || size > ATLAS_SIZE as f32 { |
| 981 | return Err(RenderError::AtlasFull); | 784 | return Err(RenderError::AtlasFull); |
| 982 | } | 785 | } |
| 786 | let color = run | ||
| 787 | .color | ||
| 788 | .map_or(ink, |color| legible(color, space.backdrop, run.backdrop)); | ||
| 789 | let [red, green, blue, _] = color; | ||
| 790 | let luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; | ||
| 791 | let tone = (f32::from(srgb_byte(luminance)) / 255.0 * 16.0).round() as u8; | ||
| 983 | for glyph in run.glyphs { | 792 | for glyph in run.glyphs { |
| 984 | let x = glyph.x * scale + origin[0]; | 793 | let x = glyph.x * scale + origin[0]; |
| 985 | let y = glyph.y * scale + origin[1]; | 794 | let y = glyph.y * scale + origin[1]; |
| ... | @@ -1000,6 +809,7 @@ impl Renderer { | ... | @@ -1000,6 +809,7 @@ impl Renderer { |
| 1000 | phase, | 809 | phase, |
| 1001 | embolden: run.embolden, | 810 | embolden: run.embolden, |
| 1002 | skew: run.skew.unwrap_or(0.0).to_bits(), | 811 | skew: run.skew.unwrap_or(0.0).to_bits(), |
| 812 | tone, | ||
| 1003 | }; | 813 | }; |
| 1004 | let cached = if let Some(cached) = self.glyphs.get(&key) { | 814 | let cached = if let Some(cached) = self.glyphs.get(&key) { |
| 1005 | *cached | 815 | *cached |
| ... | @@ -1035,7 +845,15 @@ impl Renderer { | ... | @@ -1035,7 +845,15 @@ impl Renderer { |
| 1035 | Angle::from_degrees(0.0), | 845 | Angle::from_degrees(0.0), |
| 1036 | ))); | 846 | ))); |
| 1037 | } | 847 | } |
| 1038 | let image = render.render(&mut scaler, glyph_id); | 848 | let image = render.render(&mut scaler, glyph_id).map(|mut image| { |
| 849 | if image.content == Content::Mask { | ||
| 850 | let coverage = text_coverage(tone); | ||
| 851 | for alpha in &mut image.data { | ||
| 852 | *alpha = coverage[usize::from(*alpha)]; | ||
| 853 | } | ||
| 854 | } | ||
| 855 | image | ||
| 856 | }); | ||
| 1039 | let cached = if let Some(image) = | 857 | let cached = if let Some(image) = |
| 1040 | image.filter(|i| i.placement.width > 0 && i.placement.height > 0) | 858 | image.filter(|i| i.placement.width > 0 && i.placement.height > 0) |
| 1041 | { | 859 | { |
| ... | @@ -1068,12 +886,7 @@ impl Renderer { | ... | @@ -1068,12 +886,7 @@ impl Renderer { |
| 1068 | (glyph.x + glyph.width) as f32 / atlas_size, | 886 | (glyph.x + glyph.width) as f32 / atlas_size, |
| 1069 | (glyph.y + glyph.height) as f32 / atlas_size, | 887 | (glyph.y + glyph.height) as f32 / atlas_size, |
| 1070 | ], | 888 | ], |
| 1071 | if glyph.color { | 889 | if glyph.color { [1.0; 4] } else { color }, |
| 1072 | [1.0; 4] | ||
| 1073 | } else { | ||
| 1074 | run.color | ||
| 1075 | .map_or(ink, |color| legible(color, space.backdrop, run.backdrop)) | ||
| 1076 | }, | ||
| 1077 | )?; | 890 | )?; |
| 1078 | } | 891 | } |
| 1079 | } | 892 | } |
| ... | @@ -1127,27 +940,7 @@ impl Renderer { | ... | @@ -1127,27 +940,7 @@ impl Renderer { |
| 1127 | .collect(), | 940 | .collect(), |
| 1128 | _ => image.data, | 941 | _ => image.data, |
| 1129 | }; | 942 | }; |
| 1130 | self.queue.write_texture( | 943 | self.write_atlas([cached.x, cached.y], [p.width, p.height], &rgba); |
| 1131 | wgpu::TexelCopyTextureInfo { | ||
| 1132 | origin: wgpu::Origin3d { | ||
| 1133 | x: cached.x, | ||
| 1134 | y: cached.y, | ||
| 1135 | z: 0, | ||
| 1136 | }, | ||
| 1137 | ..self.atlas.as_image_copy() | ||
| 1138 | }, | ||
| 1139 | &rgba, | ||
| 1140 | wgpu::TexelCopyBufferLayout { | ||
| 1141 | offset: 0, | ||
| 1142 | bytes_per_row: Some(p.width * 4), | ||
| 1143 | rows_per_image: Some(p.height), | ||
| 1144 | }, | ||
| 1145 | wgpu::Extent3d { | ||
| 1146 | width: p.width, | ||
| 1147 | height: p.height, | ||
| 1148 | depth_or_array_layers: 1, | ||
| 1149 | }, | ||
| 1150 | ); | ||
| 1151 | self.pen[0] += p.width + 1; | 944 | self.pen[0] += p.width + 1; |
| 1152 | self.row_height = self.row_height.max(p.height); | 945 | self.row_height = self.row_height.max(p.height); |
| 1153 | Ok(cached) | 946 | Ok(cached) |
| ... | @@ -1558,6 +1351,32 @@ fn srgb_byte(value: f32) -> u8 { | ... | @@ -1558,6 +1351,32 @@ fn srgb_byte(value: f32) -> u8 { |
| 1558 | (srgb * 255.0).round() as u8 | 1351 | (srgb * 255.0).round() as u8 |
| 1559 | } | 1352 | } |
| 1560 | 1353 | ||
| 1354 | /// Coverage for text of sRGB luminance `tone` sixteenths that, blended in linear light, | ||
| 1355 | /// darkens a white backdrop as the rasterized coverage would blended sRGB-encoded, as | ||
| 1356 | /// systems blend text: dark text keeps its weight instead of thinning, light text is | ||
| 1357 | /// nearly untouched. | ||
| 1358 | fn text_coverage(tone: u8) -> [u8; 256] { | ||
| 1359 | let encoded = f32::from(tone) / 16.0; | ||
| 1360 | let ink = linear(encoded); | ||
| 1361 | std::array::from_fn(|coverage| { | ||
| 1362 | let coverage = coverage as f32 / 255.0; | ||
| 1363 | let weighted = if ink < 0.999 { | ||
| 1364 | (1.0 - linear(1.0 - coverage * (1.0 - encoded))) / (1.0 - ink) | ||
| 1365 | } else { | ||
| 1366 | coverage | ||
| 1367 | }; | ||
| 1368 | (weighted * 255.0).round() as u8 | ||
| 1369 | }) | ||
| 1370 | } | ||
| 1371 | |||
| 1372 | fn linear(value: f32) -> f32 { | ||
| 1373 | if value <= 0.04045 { | ||
| 1374 | value / 12.92 | ||
| 1375 | } else { | ||
| 1376 | ((value + 0.055) / 1.055).powf(2.4) | ||
| 1377 | } | ||
| 1378 | } | ||
| 1379 | |||
| 1561 | /// Least OKLab lightness difference between text and the backdrop it stays legible on. | 1380 | /// Least OKLab lightness difference between text and the backdrop it stays legible on. |
| 1562 | const LEGIBLE: f32 = 0.4; | 1381 | const LEGIBLE: f32 = 0.4; |
| 1563 | 1382 | ||
| ... | @@ -1609,18 +1428,11 @@ pub fn from_oklab([lightness, a, b]: [f32; 3]) -> [f32; 3] { | ... | @@ -1609,18 +1428,11 @@ pub fn from_oklab([lightness, a, b]: [f32; 3]) -> [f32; 3] { |
| 1609 | 1428 | ||
| 1610 | /// Linear RGBA of an opaque sRGB colour. | 1429 | /// Linear RGBA of an opaque sRGB colour. |
| 1611 | pub fn srgb(red: u8, green: u8, blue: u8) -> [f32; 4] { | 1430 | pub fn srgb(red: u8, green: u8, blue: u8) -> [f32; 4] { |
| 1612 | let linear = |byte: u8| { | 1431 | let [red, green, blue] = [red, green, blue].map(|byte| linear(f32::from(byte) / 255.0)); |
| 1613 | let value = f32::from(byte) / 255.0; | 1432 | [red, green, blue, 1.0] |
| 1614 | if value <= 0.04045 { | ||
| 1615 | value / 12.92 | ||
| 1616 | } else { | ||
| 1617 | ((value + 0.055) / 1.055).powf(2.4) | ||
| 1618 | } | ||
| 1619 | }; | ||
| 1620 | [linear(red), linear(green), linear(blue), 1.0] | ||
| 1621 | } | 1433 | } |
| 1622 | 1434 | ||
| 1623 | #[cfg(test)] | 1435 | #[cfg(all(test, feature = "wgpu"))] |
| 1624 | mod tests { | 1436 | mod tests { |
| 1625 | use super::*; | 1437 | use super::*; |
| 1626 | use parley::{ | 1438 | use parley::{ |
| ... | @@ -2032,8 +1844,8 @@ mod tests { | ... | @@ -2032,8 +1844,8 @@ mod tests { |
| 2032 | let mut moment = 0.0_f64; | 1844 | let mut moment = 0.0_f64; |
| 2033 | for y in 212..252 { | 1845 | for y in 212..252 { |
| 2034 | for x in (40 + 48 * step)..(72 + 48 * step) { | 1846 | for x in (40 + 48 * step)..(72 + 48 * step) { |
| 2035 | let value = captures[0][(y * 512 + x) * 4]; | 1847 | // Black text over white leaves the rasterized coverage sRGB-encoded. |
| 2036 | let coverage = 1.0 - f64::from(srgb(value, 0, 0)[0]); | 1848 | let coverage = 1.0 - f64::from(captures[0][(y * 512 + x) * 4]) / 255.0; |
| 2037 | weight += coverage; | 1849 | weight += coverage; |
| 2038 | moment += coverage * y as f64; | 1850 | moment += coverage * y as f64; |
| 2039 | } | 1851 | } |
crates/draw/src/render/gl.rs created+469| ... | @@ -0,0 +1,469 @@ | ||
| 1 | //! Submission through OpenGL 2.1 with EXT_framebuffer_object, EXT_framebuffer_blit and | ||
| 2 | //! the sRGB extensions, which every Mac OS X 10.6 driver has and OpenGL.framework | ||
| 3 | //! exports. Frames draw into an sRGB framebuffer object, so blending happens in linear | ||
| 4 | //! light as it does in wgpu's sRGB targets. | ||
| 5 | #![allow(non_snake_case)] | ||
| 6 | use super::*; | ||
| 7 | use std::{ | ||
| 8 | ffi::{CStr, CString, c_char, c_void}, | ||
| 9 | mem::offset_of, | ||
| 10 | ptr, | ||
| 11 | }; | ||
| 12 | |||
| 13 | type GLenum = u32; | ||
| 14 | type GLuint = u32; | ||
| 15 | type GLint = i32; | ||
| 16 | type GLsizei = i32; | ||
| 17 | |||
| 18 | const TRIANGLES: GLenum = 0x0004; | ||
| 19 | const COLOR_BUFFER_BIT: GLenum = 0x4000; | ||
| 20 | const ZERO: GLenum = 0; | ||
| 21 | const ONE: GLenum = 1; | ||
| 22 | const SRC_ALPHA: GLenum = 0x0302; | ||
| 23 | const ONE_MINUS_SRC_ALPHA: GLenum = 0x0303; | ||
| 24 | const BLEND: GLenum = 0x0BE2; | ||
| 25 | const SCISSOR_TEST: GLenum = 0x0C11; | ||
| 26 | const MAX_TEXTURE_SIZE: GLenum = 0x0D33; | ||
| 27 | const TEXTURE_2D: GLenum = 0x0DE1; | ||
| 28 | const UNSIGNED_BYTE: GLenum = 0x1401; | ||
| 29 | const FLOAT: GLenum = 0x1406; | ||
| 30 | const RGBA: GLenum = 0x1908; | ||
| 31 | const NEAREST: GLint = 0x2600; | ||
| 32 | const LINEAR: GLint = 0x2601; | ||
| 33 | const TEXTURE_MAG_FILTER: GLenum = 0x2800; | ||
| 34 | const TEXTURE_MIN_FILTER: GLenum = 0x2801; | ||
| 35 | const TEXTURE_WRAP_S: GLenum = 0x2802; | ||
| 36 | const TEXTURE_WRAP_T: GLenum = 0x2803; | ||
| 37 | const CLAMP_TO_EDGE: GLint = 0x812F; | ||
| 38 | const TEXTURE0: GLenum = 0x84C0; | ||
| 39 | const ARRAY_BUFFER: GLenum = 0x8892; | ||
| 40 | const STREAM_DRAW: GLenum = 0x88E0; | ||
| 41 | const FRAGMENT_SHADER: GLenum = 0x8B30; | ||
| 42 | const VERTEX_SHADER: GLenum = 0x8B31; | ||
| 43 | const COMPILE_STATUS: GLenum = 0x8B81; | ||
| 44 | const LINK_STATUS: GLenum = 0x8B82; | ||
| 45 | const INFO_LOG_LENGTH: GLenum = 0x8B84; | ||
| 46 | const SRGB8_ALPHA8: GLint = 0x8C43; | ||
| 47 | const READ_FRAMEBUFFER: GLenum = 0x8CA8; | ||
| 48 | const DRAW_FRAMEBUFFER: GLenum = 0x8CA9; | ||
| 49 | const FRAMEBUFFER_COMPLETE: GLenum = 0x8CD5; | ||
| 50 | const COLOR_ATTACHMENT0: GLenum = 0x8CE0; | ||
| 51 | const FRAMEBUFFER: GLenum = 0x8D40; | ||
| 52 | const FRAMEBUFFER_SRGB: GLenum = 0x8DB9; | ||
| 53 | |||
| 54 | #[cfg_attr(target_os = "macos", link(name = "OpenGL", kind = "framework"))] | ||
| 55 | unsafe extern "C" { | ||
| 56 | fn glEnable(cap: GLenum); | ||
| 57 | fn glDisable(cap: GLenum); | ||
| 58 | fn glGetIntegerv(name: GLenum, value: *mut GLint); | ||
| 59 | fn glViewport(x: GLint, y: GLint, width: GLsizei, height: GLsizei); | ||
| 60 | fn glScissor(x: GLint, y: GLint, width: GLsizei, height: GLsizei); | ||
| 61 | fn glClearColor(r: f32, g: f32, b: f32, a: f32); | ||
| 62 | fn glClear(mask: GLenum); | ||
| 63 | fn glBlendFuncSeparate(src_rgb: GLenum, dst_rgb: GLenum, src_a: GLenum, dst_a: GLenum); | ||
| 64 | fn glReadPixels( | ||
| 65 | x: GLint, | ||
| 66 | y: GLint, | ||
| 67 | w: GLsizei, | ||
| 68 | h: GLsizei, | ||
| 69 | format: GLenum, | ||
| 70 | ty: GLenum, | ||
| 71 | data: *mut c_void, | ||
| 72 | ); | ||
| 73 | |||
| 74 | fn glGenTextures(n: GLsizei, textures: *mut GLuint); | ||
| 75 | fn glDeleteTextures(n: GLsizei, textures: *const GLuint); | ||
| 76 | fn glBindTexture(target: GLenum, texture: GLuint); | ||
| 77 | fn glActiveTexture(unit: GLenum); | ||
| 78 | fn glTexParameteri(target: GLenum, name: GLenum, value: GLint); | ||
| 79 | fn glTexImage2D( | ||
| 80 | target: GLenum, | ||
| 81 | level: GLint, | ||
| 82 | internal: GLint, | ||
| 83 | w: GLsizei, | ||
| 84 | h: GLsizei, | ||
| 85 | border: GLint, | ||
| 86 | format: GLenum, | ||
| 87 | ty: GLenum, | ||
| 88 | data: *const c_void, | ||
| 89 | ); | ||
| 90 | fn glTexSubImage2D( | ||
| 91 | target: GLenum, | ||
| 92 | level: GLint, | ||
| 93 | x: GLint, | ||
| 94 | y: GLint, | ||
| 95 | w: GLsizei, | ||
| 96 | h: GLsizei, | ||
| 97 | format: GLenum, | ||
| 98 | ty: GLenum, | ||
| 99 | data: *const c_void, | ||
| 100 | ); | ||
| 101 | |||
| 102 | fn glGenBuffers(n: GLsizei, buffers: *mut GLuint); | ||
| 103 | fn glBindBuffer(target: GLenum, buffer: GLuint); | ||
| 104 | fn glBufferData(target: GLenum, size: isize, data: *const c_void, usage: GLenum); | ||
| 105 | fn glEnableVertexAttribArray(index: GLuint); | ||
| 106 | fn glVertexAttribPointer( | ||
| 107 | index: GLuint, | ||
| 108 | size: GLint, | ||
| 109 | ty: GLenum, | ||
| 110 | normalized: u8, | ||
| 111 | stride: GLsizei, | ||
| 112 | offset: *const c_void, | ||
| 113 | ); | ||
| 114 | fn glDrawArrays(mode: GLenum, first: GLint, count: GLsizei); | ||
| 115 | |||
| 116 | fn glCreateShader(kind: GLenum) -> GLuint; | ||
| 117 | fn glShaderSource( | ||
| 118 | shader: GLuint, | ||
| 119 | count: GLsizei, | ||
| 120 | sources: *const *const c_char, | ||
| 121 | lengths: *const GLint, | ||
| 122 | ); | ||
| 123 | fn glCompileShader(shader: GLuint); | ||
| 124 | fn glGetShaderiv(shader: GLuint, name: GLenum, value: *mut GLint); | ||
| 125 | fn glGetShaderInfoLog(shader: GLuint, size: GLsizei, length: *mut GLsizei, log: *mut c_char); | ||
| 126 | fn glCreateProgram() -> GLuint; | ||
| 127 | fn glAttachShader(program: GLuint, shader: GLuint); | ||
| 128 | fn glBindAttribLocation(program: GLuint, index: GLuint, name: *const c_char); | ||
| 129 | fn glLinkProgram(program: GLuint); | ||
| 130 | fn glGetProgramiv(program: GLuint, name: GLenum, value: *mut GLint); | ||
| 131 | fn glGetProgramInfoLog(program: GLuint, size: GLsizei, length: *mut GLsizei, log: *mut c_char); | ||
| 132 | fn glUseProgram(program: GLuint); | ||
| 133 | fn glGetUniformLocation(program: GLuint, name: *const c_char) -> GLint; | ||
| 134 | fn glUniform1i(location: GLint, value: GLint); | ||
| 135 | |||
| 136 | fn glGenFramebuffersEXT(n: GLsizei, framebuffers: *mut GLuint); | ||
| 137 | fn glDeleteFramebuffersEXT(n: GLsizei, framebuffers: *const GLuint); | ||
| 138 | fn glBindFramebufferEXT(target: GLenum, framebuffer: GLuint); | ||
| 139 | fn glFramebufferTexture2DEXT( | ||
| 140 | target: GLenum, | ||
| 141 | attachment: GLenum, | ||
| 142 | textarget: GLenum, | ||
| 143 | texture: GLuint, | ||
| 144 | level: GLint, | ||
| 145 | ); | ||
| 146 | fn glCheckFramebufferStatusEXT(target: GLenum) -> GLenum; | ||
| 147 | fn glBlitFramebufferEXT( | ||
| 148 | sx0: GLint, | ||
| 149 | sy0: GLint, | ||
| 150 | sx1: GLint, | ||
| 151 | sy1: GLint, | ||
| 152 | dx0: GLint, | ||
| 153 | dy0: GLint, | ||
| 154 | dx1: GLint, | ||
| 155 | dy1: GLint, | ||
| 156 | mask: GLenum, | ||
| 157 | filter: GLenum, | ||
| 158 | ); | ||
| 159 | } | ||
| 160 | |||
| 161 | /// An sRGB RGBA texture, deleted with its value. | ||
| 162 | pub(super) struct Image { | ||
| 163 | name: GLuint, | ||
| 164 | size: [u32; 2], | ||
| 165 | } | ||
| 166 | |||
| 167 | impl Image { | ||
| 168 | fn new(size: [u32; 2], filter: GLint, pixels: Option<&[u8]>) -> Self { | ||
| 169 | let mut name = 0; | ||
| 170 | unsafe { | ||
| 171 | glGenTextures(1, &mut name); | ||
| 172 | glBindTexture(TEXTURE_2D, name); | ||
| 173 | glTexParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, filter); | ||
| 174 | glTexParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, filter); | ||
| 175 | glTexParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE); | ||
| 176 | glTexParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE); | ||
| 177 | glTexImage2D( | ||
| 178 | TEXTURE_2D, | ||
| 179 | 0, | ||
| 180 | SRGB8_ALPHA8, | ||
| 181 | size[0] as GLsizei, | ||
| 182 | size[1] as GLsizei, | ||
| 183 | 0, | ||
| 184 | RGBA, | ||
| 185 | UNSIGNED_BYTE, | ||
| 186 | pixels.map_or(ptr::null(), |pixels| pixels.as_ptr().cast()), | ||
| 187 | ); | ||
| 188 | } | ||
| 189 | Self { name, size } | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | impl Drop for Image { | ||
| 194 | fn drop(&mut self) { | ||
| 195 | unsafe { glDeleteTextures(1, &self.name) }; | ||
| 196 | } | ||
| 197 | } | ||
| 198 | |||
| 199 | /// An offscreen frame a window shows by `present`, the context's default framebuffer | ||
| 200 | /// being no sRGB target. | ||
| 201 | pub struct Target { | ||
| 202 | framebuffer: GLuint, | ||
| 203 | texture: Image, | ||
| 204 | } | ||
| 205 | |||
| 206 | impl Target { | ||
| 207 | /// Needs the renderer's context current, as every method here does. | ||
| 208 | pub fn new(size: [u32; 2]) -> Result<Self, String> { | ||
| 209 | let texture = Image::new(size, NEAREST, None); | ||
| 210 | let mut framebuffer = 0; | ||
| 211 | unsafe { | ||
| 212 | glGenFramebuffersEXT(1, &mut framebuffer); | ||
| 213 | glBindFramebufferEXT(FRAMEBUFFER, framebuffer); | ||
| 214 | glFramebufferTexture2DEXT(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, texture.name, 0); | ||
| 215 | let status = glCheckFramebufferStatusEXT(FRAMEBUFFER); | ||
| 216 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 217 | if status != FRAMEBUFFER_COMPLETE { | ||
| 218 | glDeleteFramebuffersEXT(1, &framebuffer); | ||
| 219 | return Err(format!("Framebuffer incomplete: {status:#x}")); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | Ok(Self { | ||
| 223 | framebuffer, | ||
| 224 | texture, | ||
| 225 | }) | ||
| 226 | } | ||
| 227 | |||
| 228 | pub fn size(&self) -> [u32; 2] { | ||
| 229 | self.texture.size | ||
| 230 | } | ||
| 231 | |||
| 232 | /// Copies the frame to the context's window, which is the same size. | ||
| 233 | pub fn present(&self) { | ||
| 234 | let [width, height] = self.size().map(|side| side as GLint); | ||
| 235 | unsafe { | ||
| 236 | glBindFramebufferEXT(READ_FRAMEBUFFER, self.framebuffer); | ||
| 237 | glBindFramebufferEXT(DRAW_FRAMEBUFFER, 0); | ||
| 238 | glBlitFramebufferEXT( | ||
| 239 | 0, | ||
| 240 | 0, | ||
| 241 | width, | ||
| 242 | height, | ||
| 243 | 0, | ||
| 244 | 0, | ||
| 245 | width, | ||
| 246 | height, | ||
| 247 | COLOR_BUFFER_BIT, | ||
| 248 | NEAREST as GLenum, | ||
| 249 | ); | ||
| 250 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | /// sRGB-encoded RGBA rows, top first. | ||
| 255 | pub fn read_pixels(&self) -> Vec<u8> { | ||
| 256 | let [width, height] = self.size(); | ||
| 257 | let mut pixels = vec![0; (width * height * 4) as usize]; | ||
| 258 | unsafe { | ||
| 259 | glBindFramebufferEXT(FRAMEBUFFER, self.framebuffer); | ||
| 260 | glReadPixels( | ||
| 261 | 0, | ||
| 262 | 0, | ||
| 263 | width as GLsizei, | ||
| 264 | height as GLsizei, | ||
| 265 | RGBA, | ||
| 266 | UNSIGNED_BYTE, | ||
| 267 | pixels.as_mut_ptr().cast(), | ||
| 268 | ); | ||
| 269 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 270 | } | ||
| 271 | pixels | ||
| 272 | .chunks(width as usize * 4) | ||
| 273 | .rev() | ||
| 274 | .flatten() | ||
| 275 | .copied() | ||
| 276 | .collect() | ||
| 277 | } | ||
| 278 | } | ||
| 279 | |||
| 280 | impl Drop for Target { | ||
| 281 | fn drop(&mut self) { | ||
| 282 | unsafe { glDeleteFramebuffersEXT(1, &self.framebuffer) }; | ||
| 283 | } | ||
| 284 | } | ||
| 285 | |||
| 286 | pub(super) struct Gpu { | ||
| 287 | program: GLuint, | ||
| 288 | buffer: GLuint, | ||
| 289 | atlas: Image, | ||
| 290 | max_texture: u32, | ||
| 291 | } | ||
| 292 | |||
| 293 | /// Each attribute's name, component count and offset, bound to its index in the shader. | ||
| 294 | const ATTRIBUTES: [(&CStr, GLint, usize); 6] = [ | ||
| 295 | (c"position", 2, offset_of!(Vertex, position)), | ||
| 296 | (c"uv", 2, offset_of!(Vertex, uv)), | ||
| 297 | (c"color", 4, offset_of!(Vertex, color)), | ||
| 298 | (c"local", 2, offset_of!(Vertex, local)), | ||
| 299 | (c"shape", 4, offset_of!(Vertex, shape)), | ||
| 300 | (c"stroke", 1, offset_of!(Vertex, stroke)), | ||
| 301 | ]; | ||
| 302 | |||
| 303 | impl Renderer { | ||
| 304 | /// Draws with the OpenGL context current on this thread, which must stay current | ||
| 305 | /// whenever the renderer or a `Target` is used. | ||
| 306 | pub fn new() -> Result<Self, String> { | ||
| 307 | let source = include_str!("../draw.glsl"); | ||
| 308 | let program = unsafe { | ||
| 309 | let vertex = compile( | ||
| 310 | VERTEX_SHADER, | ||
| 311 | &format!("#version 120\n#define VERTEX\n{source}"), | ||
| 312 | )?; | ||
| 313 | let fragment = compile(FRAGMENT_SHADER, &format!("#version 120\n{source}"))?; | ||
| 314 | let program = glCreateProgram(); | ||
| 315 | glAttachShader(program, vertex); | ||
| 316 | glAttachShader(program, fragment); | ||
| 317 | for (index, (name, _, _)) in ATTRIBUTES.iter().enumerate() { | ||
| 318 | glBindAttribLocation(program, index as GLuint, name.as_ptr()); | ||
| 319 | } | ||
| 320 | glLinkProgram(program); | ||
| 321 | let mut linked = 0; | ||
| 322 | glGetProgramiv(program, LINK_STATUS, &mut linked); | ||
| 323 | if linked == 0 { | ||
| 324 | return Err(format!( | ||
| 325 | "Linking the draw shader failed: {}", | ||
| 326 | log(program, glGetProgramiv, glGetProgramInfoLog) | ||
| 327 | )); | ||
| 328 | } | ||
| 329 | program | ||
| 330 | }; | ||
| 331 | let mut buffer = 0; | ||
| 332 | let mut max_texture = 0; | ||
| 333 | unsafe { | ||
| 334 | glGenBuffers(1, &mut buffer); | ||
| 335 | glGetIntegerv(MAX_TEXTURE_SIZE, &mut max_texture); | ||
| 336 | } | ||
| 337 | Ok(Self::with_gpu(Gpu { | ||
| 338 | program, | ||
| 339 | buffer, | ||
| 340 | atlas: Image::new([ATLAS_SIZE; 2], NEAREST, None), | ||
| 341 | max_texture: max_texture as u32, | ||
| 342 | })) | ||
| 343 | } | ||
| 344 | |||
| 345 | /// The widest and tallest texture the driver takes, in pixels. | ||
| 346 | pub fn max_texture_dimension(&self) -> u32 { | ||
| 347 | self.gpu.max_texture | ||
| 348 | } | ||
| 349 | |||
| 350 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { | ||
| 351 | unsafe { | ||
| 352 | glBindTexture(TEXTURE_2D, self.gpu.atlas.name); | ||
| 353 | glTexSubImage2D( | ||
| 354 | TEXTURE_2D, | ||
| 355 | 0, | ||
| 356 | origin[0] as GLint, | ||
| 357 | origin[1] as GLint, | ||
| 358 | size[0] as GLsizei, | ||
| 359 | size[1] as GLsizei, | ||
| 360 | RGBA, | ||
| 361 | UNSIGNED_BYTE, | ||
| 362 | rgba.as_ptr().cast(), | ||
| 363 | ); | ||
| 364 | } | ||
| 365 | } | ||
| 366 | |||
| 367 | pub(super) fn upload_image(&self, image: &RasterImage) -> Image { | ||
| 368 | Image::new(image.size, LINEAR, Some(image.pixels())) | ||
| 369 | } | ||
| 370 | |||
| 371 | /// Clears `target` to linear `clear` and draws the prepared batches. | ||
| 372 | pub(super) fn submit(&mut self, target: &Target, clear: [f32; 4]) { | ||
| 373 | let [width, height] = target.size().map(|side| side as GLsizei); | ||
| 374 | let gpu = &self.gpu; | ||
| 375 | unsafe { | ||
| 376 | glBindFramebufferEXT(FRAMEBUFFER, target.framebuffer); | ||
| 377 | glEnable(FRAMEBUFFER_SRGB); | ||
| 378 | glViewport(0, 0, width, height); | ||
| 379 | glDisable(SCISSOR_TEST); | ||
| 380 | glClearColor(clear[0], clear[1], clear[2], clear[3]); | ||
| 381 | glClear(COLOR_BUFFER_BIT); | ||
| 382 | glUseProgram(gpu.program); | ||
| 383 | glUniform1i(glGetUniformLocation(gpu.program, c"atlas".as_ptr()), 0); | ||
| 384 | glActiveTexture(TEXTURE0); | ||
| 385 | glBindBuffer(ARRAY_BUFFER, gpu.buffer); | ||
| 386 | glBufferData( | ||
| 387 | ARRAY_BUFFER, | ||
| 388 | size_of_val(self.vertices.as_slice()) as isize, | ||
| 389 | self.vertices.as_ptr().cast(), | ||
| 390 | STREAM_DRAW, | ||
| 391 | ); | ||
| 392 | for (index, (_, components, offset)) in ATTRIBUTES.iter().enumerate() { | ||
| 393 | glEnableVertexAttribArray(index as GLuint); | ||
| 394 | glVertexAttribPointer( | ||
| 395 | index as GLuint, | ||
| 396 | *components, | ||
| 397 | FLOAT, | ||
| 398 | 0, | ||
| 399 | size_of::<Vertex>() as GLsizei, | ||
| 400 | ptr::without_provenance(*offset), | ||
| 401 | ); | ||
| 402 | } | ||
| 403 | glEnable(BLEND); | ||
| 404 | glEnable(SCISSOR_TEST); | ||
| 405 | for batch in &self.batches { | ||
| 406 | let [x, y, w, h] = batch.scissor.map(|value| value as GLint); | ||
| 407 | glScissor(x, height - y - h, w, h); | ||
| 408 | let (texture, [src_rgb, dst_rgb, src_alpha, dst_alpha]) = match batch.blend { | ||
| 409 | Blend::Over => ( | ||
| 410 | &gpu.atlas, | ||
| 411 | [SRC_ALPHA, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA], | ||
| 412 | ), | ||
| 413 | Blend::Image(id) => ( | ||
| 414 | &self.images[&id].texture, | ||
| 415 | [ONE, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA], | ||
| 416 | ), | ||
| 417 | Blend::Erase => ( | ||
| 418 | &gpu.atlas, | ||
| 419 | [ZERO, ONE_MINUS_SRC_ALPHA, ZERO, ONE_MINUS_SRC_ALPHA], | ||
| 420 | ), | ||
| 421 | }; | ||
| 422 | glBlendFuncSeparate(src_rgb, dst_rgb, src_alpha, dst_alpha); | ||
| 423 | glBindTexture(TEXTURE_2D, texture.name); | ||
| 424 | glDrawArrays( | ||
| 425 | TRIANGLES, | ||
| 426 | batch.vertices.start as GLint, | ||
| 427 | batch.vertices.len() as GLsizei, | ||
| 428 | ); | ||
| 429 | } | ||
| 430 | glDisable(SCISSOR_TEST); | ||
| 431 | glDisable(BLEND); | ||
| 432 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 433 | } | ||
| 434 | } | ||
| 435 | } | ||
| 436 | |||
| 437 | unsafe fn compile(kind: GLenum, source: &str) -> Result<GLuint, String> { | ||
| 438 | unsafe { | ||
| 439 | let shader = glCreateShader(kind); | ||
| 440 | let text = CString::new(source).expect("The shader has no NUL"); | ||
| 441 | glShaderSource(shader, 1, &text.as_ptr(), ptr::null()); | ||
| 442 | glCompileShader(shader); | ||
| 443 | let mut compiled = 0; | ||
| 444 | glGetShaderiv(shader, COMPILE_STATUS, &mut compiled); | ||
| 445 | if compiled == 0 { | ||
| 446 | return Err(format!( | ||
| 447 | "Compiling the draw shader failed: {}", | ||
| 448 | log(shader, glGetShaderiv, glGetShaderInfoLog) | ||
| 449 | )); | ||
| 450 | } | ||
| 451 | Ok(shader) | ||
| 452 | } | ||
| 453 | } | ||
| 454 | |||
| 455 | unsafe fn log( | ||
| 456 | object: GLuint, | ||
| 457 | get: unsafe extern "C" fn(GLuint, GLenum, *mut GLint), | ||
| 458 | read: unsafe extern "C" fn(GLuint, GLsizei, *mut GLsizei, *mut c_char), | ||
| 459 | ) -> String { | ||
| 460 | unsafe { | ||
| 461 | let mut length = 0; | ||
| 462 | get(object, INFO_LOG_LENGTH, &mut length); | ||
| 463 | let mut text = vec![0u8; length.max(1) as usize]; | ||
| 464 | read(object, length, ptr::null_mut(), text.as_mut_ptr().cast()); | ||
| 465 | String::from_utf8_lossy(&text) | ||
| 466 | .trim_end_matches('\0') | ||
| 467 | .to_owned() | ||
| 468 | } | ||
| 469 | } | ||
crates/draw/src/render/webgpu.rs created+265| ... | @@ -0,0 +1,265 @@ | ||
| 1 | //! Submission through wgpu: Metal, Vulkan or GL, whichever the platform offers. | ||
| 2 | use super::*; | ||
| 3 | |||
| 4 | /// What a frame is drawn into. | ||
| 5 | pub type Target = wgpu::TextureView; | ||
| 6 | |||
| 7 | pub(super) type Image = wgpu::BindGroup; | ||
| 8 | |||
| 9 | pub(super) struct Gpu { | ||
| 10 | pipeline: wgpu::RenderPipeline, | ||
| 11 | image_pipeline: wgpu::RenderPipeline, | ||
| 12 | erase_pipeline: wgpu::RenderPipeline, | ||
| 13 | atlas: wgpu::Texture, | ||
| 14 | bind_group: wgpu::BindGroup, | ||
| 15 | image_sampler: wgpu::Sampler, | ||
| 16 | vertex_buffer: wgpu::Buffer, | ||
| 17 | } | ||
| 18 | |||
| 19 | impl Renderer { | ||
| 20 | pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat) -> Self { | ||
| 21 | let shader = device.create_shader_module(wgpu::include_wgsl!("../draw.wgsl")); | ||
| 22 | let binding_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { | ||
| 23 | label: Some("Draw texture"), | ||
| 24 | entries: &[ | ||
| 25 | wgpu::BindGroupLayoutEntry { | ||
| 26 | binding: 0, | ||
| 27 | visibility: wgpu::ShaderStages::FRAGMENT, | ||
| 28 | ty: wgpu::BindingType::Texture { | ||
| 29 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, | ||
| 30 | view_dimension: wgpu::TextureViewDimension::D2, | ||
| 31 | multisampled: false, | ||
| 32 | }, | ||
| 33 | count: None, | ||
| 34 | }, | ||
| 35 | wgpu::BindGroupLayoutEntry { | ||
| 36 | binding: 1, | ||
| 37 | visibility: wgpu::ShaderStages::FRAGMENT, | ||
| 38 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), | ||
| 39 | count: None, | ||
| 40 | }, | ||
| 41 | ], | ||
| 42 | }); | ||
| 43 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { | ||
| 44 | label: Some("Draw"), | ||
| 45 | bind_group_layouts: &[Some(&binding_layout)], | ||
| 46 | ..Default::default() | ||
| 47 | }); | ||
| 48 | let make_pipeline = |blend| { | ||
| 49 | device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { | ||
| 50 | label: Some("Draw"), | ||
| 51 | layout: Some(&pipeline_layout), | ||
| 52 | vertex: wgpu::VertexState { | ||
| 53 | module: &shader, | ||
| 54 | entry_point: Some("vertex"), | ||
| 55 | compilation_options: Default::default(), | ||
| 56 | buffers: &[Some(wgpu::VertexBufferLayout { | ||
| 57 | array_stride: size_of::<Vertex>() as u64, | ||
| 58 | step_mode: wgpu::VertexStepMode::Vertex, | ||
| 59 | attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32], | ||
| 60 | })], | ||
| 61 | }, | ||
| 62 | fragment: Some(wgpu::FragmentState { | ||
| 63 | module: &shader, | ||
| 64 | entry_point: Some("fragment"), | ||
| 65 | compilation_options: Default::default(), | ||
| 66 | targets: &[Some(wgpu::ColorTargetState { | ||
| 67 | format, | ||
| 68 | blend: Some(blend), | ||
| 69 | write_mask: wgpu::ColorWrites::ALL, | ||
| 70 | })], | ||
| 71 | }), | ||
| 72 | primitive: Default::default(), | ||
| 73 | depth_stencil: None, | ||
| 74 | multisample: Default::default(), | ||
| 75 | multiview_mask: None, | ||
| 76 | cache: None, | ||
| 77 | }) | ||
| 78 | }; | ||
| 79 | // Coverage accumulates in alpha, leaving premultiplied colour over a transparent clear. | ||
| 80 | let pipeline = make_pipeline(wgpu::BlendState { | ||
| 81 | color: wgpu::BlendState::ALPHA_BLENDING.color, | ||
| 82 | alpha: wgpu::BlendComponent::OVER, | ||
| 83 | }); | ||
| 84 | let image_pipeline = make_pipeline(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING); | ||
| 85 | let erase = wgpu::BlendComponent { | ||
| 86 | src_factor: wgpu::BlendFactor::Zero, | ||
| 87 | dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, | ||
| 88 | operation: wgpu::BlendOperation::Add, | ||
| 89 | }; | ||
| 90 | let erase_pipeline = make_pipeline(wgpu::BlendState { | ||
| 91 | color: erase, | ||
| 92 | alpha: erase, | ||
| 93 | }); | ||
| 94 | let atlas = device.create_texture(&wgpu::TextureDescriptor { | ||
| 95 | label: Some("Glyph atlas"), | ||
| 96 | size: wgpu::Extent3d { | ||
| 97 | width: ATLAS_SIZE, | ||
| 98 | height: ATLAS_SIZE, | ||
| 99 | depth_or_array_layers: 1, | ||
| 100 | }, | ||
| 101 | mip_level_count: 1, | ||
| 102 | sample_count: 1, | ||
| 103 | dimension: wgpu::TextureDimension::D2, | ||
| 104 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | ||
| 105 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | ||
| 106 | view_formats: &[], | ||
| 107 | }); | ||
| 108 | let view = atlas.create_view(&Default::default()); | ||
| 109 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { | ||
| 110 | mag_filter: wgpu::FilterMode::Nearest, | ||
| 111 | min_filter: wgpu::FilterMode::Nearest, | ||
| 112 | ..Default::default() | ||
| 113 | }); | ||
| 114 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { | ||
| 115 | label: Some("Glyph atlas"), | ||
| 116 | layout: &pipeline.get_bind_group_layout(0), | ||
| 117 | entries: &[ | ||
| 118 | wgpu::BindGroupEntry { | ||
| 119 | binding: 0, | ||
| 120 | resource: wgpu::BindingResource::TextureView(&view), | ||
| 121 | }, | ||
| 122 | wgpu::BindGroupEntry { | ||
| 123 | binding: 1, | ||
| 124 | resource: wgpu::BindingResource::Sampler(&sampler), | ||
| 125 | }, | ||
| 126 | ], | ||
| 127 | }); | ||
| 128 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { | ||
| 129 | label: Some("Draw vertices"), | ||
| 130 | size: VERTEX_BUFFER_BYTES, | ||
| 131 | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, | ||
| 132 | mapped_at_creation: false, | ||
| 133 | }); | ||
| 134 | let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor { | ||
| 135 | label: Some("Draw image filtering"), | ||
| 136 | mag_filter: wgpu::FilterMode::Linear, | ||
| 137 | min_filter: wgpu::FilterMode::Linear, | ||
| 138 | ..Default::default() | ||
| 139 | }); | ||
| 140 | Self::with_gpu( | ||
| 141 | device, | ||
| 142 | queue, | ||
| 143 | Gpu { | ||
| 144 | pipeline, | ||
| 145 | image_pipeline, | ||
| 146 | erase_pipeline, | ||
| 147 | atlas, | ||
| 148 | bind_group, | ||
| 149 | image_sampler, | ||
| 150 | vertex_buffer, | ||
| 151 | }, | ||
| 152 | ) | ||
| 153 | } | ||
| 154 | |||
| 155 | /// The widest and tallest texture the device takes, in pixels. | ||
| 156 | pub fn max_texture_dimension(&self) -> u32 { | ||
| 157 | self.device.limits().max_texture_dimension_2d | ||
| 158 | } | ||
| 159 | |||
| 160 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { | ||
| 161 | self.queue.write_texture( | ||
| 162 | wgpu::TexelCopyTextureInfo { | ||
| 163 | origin: wgpu::Origin3d { | ||
| 164 | x: origin[0], | ||
| 165 | y: origin[1], | ||
| 166 | z: 0, | ||
| 167 | }, | ||
| 168 | ..self.gpu.atlas.as_image_copy() | ||
| 169 | }, | ||
| 170 | rgba, | ||
| 171 | wgpu::TexelCopyBufferLayout { | ||
| 172 | offset: 0, | ||
| 173 | bytes_per_row: Some(size[0] * 4), | ||
| 174 | rows_per_image: Some(size[1]), | ||
| 175 | }, | ||
| 176 | wgpu::Extent3d { | ||
| 177 | width: size[0], | ||
| 178 | height: size[1], | ||
| 179 | depth_or_array_layers: 1, | ||
| 180 | }, | ||
| 181 | ); | ||
| 182 | } | ||
| 183 | |||
| 184 | pub(super) fn upload_image(&self, image: &RasterImage) -> Image { | ||
| 185 | let size = wgpu::Extent3d { | ||
| 186 | width: image.size[0], | ||
| 187 | height: image.size[1], | ||
| 188 | depth_or_array_layers: 1, | ||
| 189 | }; | ||
| 190 | let texture = self.device.create_texture(&wgpu::TextureDescriptor { | ||
| 191 | label: Some("Draw image"), | ||
| 192 | size, | ||
| 193 | mip_level_count: 1, | ||
| 194 | sample_count: 1, | ||
| 195 | dimension: wgpu::TextureDimension::D2, | ||
| 196 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | ||
| 197 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | ||
| 198 | view_formats: &[], | ||
| 199 | }); | ||
| 200 | self.queue.write_texture( | ||
| 201 | texture.as_image_copy(), | ||
| 202 | image.pixels(), | ||
| 203 | wgpu::TexelCopyBufferLayout { | ||
| 204 | offset: 0, | ||
| 205 | bytes_per_row: Some(image.size[0] * 4), | ||
| 206 | rows_per_image: Some(image.size[1]), | ||
| 207 | }, | ||
| 208 | size, | ||
| 209 | ); | ||
| 210 | self.device.create_bind_group(&wgpu::BindGroupDescriptor { | ||
| 211 | label: Some("Draw image"), | ||
| 212 | layout: &self.gpu.pipeline.get_bind_group_layout(0), | ||
| 213 | entries: &[ | ||
| 214 | wgpu::BindGroupEntry { | ||
| 215 | binding: 0, | ||
| 216 | resource: wgpu::BindingResource::TextureView( | ||
| 217 | &texture.create_view(&Default::default()), | ||
| 218 | ), | ||
| 219 | }, | ||
| 220 | wgpu::BindGroupEntry { | ||
| 221 | binding: 1, | ||
| 222 | resource: wgpu::BindingResource::Sampler(&self.gpu.image_sampler), | ||
| 223 | }, | ||
| 224 | ], | ||
| 225 | }) | ||
| 226 | } | ||
| 227 | |||
| 228 | /// Clears `target` to linear `clear` and draws the prepared batches. | ||
| 229 | pub(super) fn submit(&mut self, target: &Target, clear: [f32; 4]) { | ||
| 230 | let gpu = &self.gpu; | ||
| 231 | self.queue | ||
| 232 | .write_buffer(&gpu.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices)); | ||
| 233 | let mut encoder = self.device.create_command_encoder(&Default::default()); | ||
| 234 | { | ||
| 235 | let [r, g, b, a] = clear.map(f64::from); | ||
| 236 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { | ||
| 237 | label: Some("Draw"), | ||
| 238 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { | ||
| 239 | view: target, | ||
| 240 | depth_slice: None, | ||
| 241 | resolve_target: None, | ||
| 242 | ops: wgpu::Operations { | ||
| 243 | load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a }), | ||
| 244 | store: wgpu::StoreOp::Store, | ||
| 245 | }, | ||
| 246 | })], | ||
| 247 | ..Default::default() | ||
| 248 | }); | ||
| 249 | pass.set_vertex_buffer(0, gpu.vertex_buffer.slice(..)); | ||
| 250 | for batch in &self.batches { | ||
| 251 | let [x, y, width, height] = batch.scissor; | ||
| 252 | pass.set_scissor_rect(x, y, width, height); | ||
| 253 | let (pipeline, binding) = match batch.blend { | ||
| 254 | Blend::Over => (&gpu.pipeline, &gpu.bind_group), | ||
| 255 | Blend::Image(id) => (&gpu.image_pipeline, &self.images[&id].texture), | ||
| 256 | Blend::Erase => (&gpu.erase_pipeline, &gpu.bind_group), | ||
| 257 | }; | ||
| 258 | pass.set_pipeline(pipeline); | ||
| 259 | pass.set_bind_group(0, binding, &[]); | ||
| 260 | pass.draw(batch.vertices.clone(), 0..1); | ||
| 261 | } | ||
| 262 | } | ||
| 263 | self.queue.submit([encoder.finish()]); | ||
| 264 | } | ||
| 265 | } | ||
crates/notebook/README.md+3| ... | @@ -60,6 +60,9 @@ failures; `notify` runs on a background thread whenever an event waits. | ... | @@ -60,6 +60,9 @@ failures; `notify` runs on a background thread whenever an event waits. |
| 60 | removes; `release(id, archive, Resolution)` ends an uncertain attempt. The author an | 60 | removes; `release(id, archive, Resolution)` ends an uncertain attempt. The author an |
| 61 | edit names is the host's: the app passes the account's full name. | 61 | edit names is the host's: the app passes the account's full name. |
| 62 | `import_page` creates a page holding a copy of another; `delete_pages` removes pages. | 62 | `import_page` creates a page holding a copy of another; `delete_pages` removes pages. |
| 63 | `sync_status()` gives when the section file was last reached, why it could not be since | ||
| 64 | and how many edits wait for it. `set_offline(true)` works offline as OneNote does: the | ||
| 65 | worker stops connecting and edits queue until `wake()` (Sync Now) or `set_offline(false)`. | ||
| 63 | 66 | ||
| 64 | `Section::resume(file, replica, notify)` starts from an owned `Replica` without | 67 | `Section::resume(file, replica, notify)` starts from an owned `Replica` without |
| 65 | consulting the remote file; with the `smb` feature, `Section::resume_smb(path, | 68 | consulting the remote file; with the `smb` feature, `Section::resume_smb(path, |
crates/notebook/src/session.rs+60-2| ... | @@ -1028,6 +1028,20 @@ pub enum Event { | ... | @@ -1028,6 +1028,20 @@ pub enum Event { |
| 1028 | 1028 | ||
| 1029 | type Notify = Arc<dyn Fn() + Send + Sync>; | 1029 | type Notify = Arc<dyn Fn() + Send + Sync>; |
| 1030 | 1030 | ||
| 1031 | /// How a section's synchronization stands, for the host to show. | ||
| 1032 | #[derive(Debug)] | ||
| 1033 | pub struct SyncStatus { | ||
| 1034 | /// FILETIME of the last synchronization step that reached the section file. | ||
| 1035 | pub synced: Option<u64>, | ||
| 1036 | /// Why the last step failed, until one reaches the section file again. | ||
| 1037 | pub error: Option<io::Error>, | ||
| 1038 | /// Edits the section file does not hold yet, uncertain attempts included. | ||
| 1039 | pub queued: u64, | ||
| 1040 | } | ||
| 1041 | |||
| 1042 | /// The last attempt's outcome; `None` before the first. | ||
| 1043 | type Observed = Option<Option<io::Error>>; | ||
| 1044 | |||
| 1031 | /// A section file with its replica and background publication. `Send + Sync`: edits | 1045 | /// A section file with its replica and background publication. `Send + Sync`: edits |
| 1032 | /// arrive from any thread without waiting, and `notify` wakes the host when events wait. | 1046 | /// arrive from any thread without waiting, and `notify` wakes the host when events wait. |
| 1033 | pub struct Section { | 1047 | pub struct Section { |
| ... | @@ -1037,6 +1051,7 @@ pub struct Section { | ... | @@ -1037,6 +1051,7 @@ pub struct Section { |
| 1037 | events: Mutex<Receiver<Event>>, | 1051 | events: Mutex<Receiver<Event>>, |
| 1038 | sender: Sender<Event>, | 1052 | sender: Sender<Event>, |
| 1039 | notify: Notify, | 1053 | notify: Notify, |
| 1054 | observed: Arc<Mutex<Observed>>, | ||
| 1040 | } | 1055 | } |
| 1041 | 1056 | ||
| 1042 | impl Section { | 1057 | impl Section { |
| ... | @@ -1129,9 +1144,24 @@ impl Section { | ... | @@ -1129,9 +1144,24 @@ impl Section { |
| 1129 | notify(); | 1144 | notify(); |
| 1130 | } | 1145 | } |
| 1131 | }); | 1146 | }); |
| 1147 | let observed: Arc<Mutex<Observed>> = Arc::new(Mutex::new(None)); | ||
| 1132 | let worker = { | 1148 | let worker = { |
| 1133 | let (sender, notify) = (sender.clone(), Arc::clone(&notify)); | 1149 | let (sender, notify) = (sender.clone(), Arc::clone(&notify)); |
| 1150 | let observed = Arc::clone(&observed); | ||
| 1134 | replica.start_sync(Duration::from_secs(2), connect, move |result| { | 1151 | replica.start_sync(Duration::from_secs(2), connect, move |result| { |
| 1152 | let error = result.as_ref().err().map(|error| match error { | ||
| 1153 | Error::RemoteIo(error) => io::Error::new(error.kind(), error.to_string()), | ||
| 1154 | Error::Remote(error) => io::Error::new(error.error.kind(), error.to_string()), | ||
| 1155 | error => io::Error::other(error.to_string()), | ||
| 1156 | }); | ||
| 1157 | // Reaching the file as the last attempt did is no news to the host. | ||
| 1158 | let mut changed = false; | ||
| 1159 | if let Ok(mut observed) = observed.lock() { | ||
| 1160 | changed = observed | ||
| 1161 | .as_ref() | ||
| 1162 | .is_none_or(|last| last.is_some() != error.is_some()); | ||
| 1163 | *observed = Some(error); | ||
| 1164 | } | ||
| 1135 | let mut events = Vec::new(); | 1165 | let mut events = Vec::new(); |
| 1136 | match result { | 1166 | match result { |
| 1137 | Ok(synced) => { | 1167 | Ok(synced) => { |
| ... | @@ -1157,7 +1187,8 @@ impl Section { | ... | @@ -1157,7 +1187,8 @@ impl Section { |
| 1157 | ))), | 1187 | ))), |
| 1158 | Err(error) => events.push(Event::Failed(error.to_string())), | 1188 | Err(error) => events.push(Event::Failed(error.to_string())), |
| 1159 | } | 1189 | } |
| 1160 | if !events.is_empty() && events.into_iter().all(|event| sender.send(event).is_ok()) | 1190 | if (changed || !events.is_empty()) |
| 1191 | && events.into_iter().all(|event| sender.send(event).is_ok()) | ||
| 1161 | { | 1192 | { |
| 1162 | notify(); | 1193 | notify(); |
| 1163 | } | 1194 | } |
| ... | @@ -1170,6 +1201,7 @@ impl Section { | ... | @@ -1170,6 +1201,7 @@ impl Section { |
| 1170 | events: Mutex::new(events), | 1201 | events: Mutex::new(events), |
| 1171 | sender, | 1202 | sender, |
| 1172 | notify, | 1203 | notify, |
| 1204 | observed, | ||
| 1173 | }) | 1205 | }) |
| 1174 | } | 1206 | } |
| 1175 | 1207 | ||
| ... | @@ -1332,13 +1364,39 @@ impl Section { | ... | @@ -1332,13 +1364,39 @@ impl Section { |
| 1332 | .unwrap_or_default() | 1364 | .unwrap_or_default() |
| 1333 | } | 1365 | } |
| 1334 | 1366 | ||
| 1335 | /// Requests a synchronization attempt now. | 1367 | /// Requests a synchronization attempt now, working offline included (Sync Now). |
| 1336 | pub fn wake(&self) { | 1368 | pub fn wake(&self) { |
| 1337 | if let Some(worker) = &self.worker { | 1369 | if let Some(worker) = &self.worker { |
| 1338 | worker.wake(); | 1370 | worker.wake(); |
| 1339 | } | 1371 | } |
| 1340 | } | 1372 | } |
| 1341 | 1373 | ||
| 1374 | /// Stops or resumes synchronizing: working offline, edits queue until `wake` or until | ||
| 1375 | /// working online again (OneNote's Work Offline). | ||
| 1376 | pub fn set_offline(&self, offline: bool) { | ||
| 1377 | if let Some(worker) = &self.worker { | ||
| 1378 | worker.set_offline(offline); | ||
| 1379 | } | ||
| 1380 | } | ||
| 1381 | |||
| 1382 | /// When the section file was last reached, why it could not be since, and what waits | ||
| 1383 | /// for it. `notify` runs when it is first reached and when an error comes or goes. | ||
| 1384 | pub fn sync_status(&self) -> Result<SyncStatus> { | ||
| 1385 | let queued = self.replica.recovery_summary()?.queued_edits; | ||
| 1386 | let observed = self | ||
| 1387 | .observed | ||
| 1388 | .lock() | ||
| 1389 | .map_err(|_| io::Error::other("Synchronization observer panicked"))?; | ||
| 1390 | Ok(SyncStatus { | ||
| 1391 | synced: self.worker.as_ref().and_then(SyncWorker::synced), | ||
| 1392 | error: observed | ||
| 1393 | .as_ref() | ||
| 1394 | .and_then(Option::as_ref) | ||
| 1395 | .map(|error| io::Error::new(error.kind(), error.to_string())), | ||
| 1396 | queued, | ||
| 1397 | }) | ||
| 1398 | } | ||
| 1399 | |||
| 1342 | /// The replica, which other threads may read pages from while the section is open. | 1400 | /// The replica, which other threads may read pages from while the section is open. |
| 1343 | pub fn replica(&self) -> &Arc<Replica> { | 1401 | pub fn replica(&self) -> &Arc<Replica> { |
| 1344 | &self.replica | 1402 | &self.replica |
crates/notebook/src/worker.rs+38-2| ... | @@ -4,7 +4,7 @@ use std::{ | ... | @@ -4,7 +4,7 @@ use std::{ |
| 4 | hash::BuildHasher, | 4 | hash::BuildHasher, |
| 5 | sync::{ | 5 | sync::{ |
| 6 | Arc, | 6 | Arc, |
| 7 | atomic::{AtomicBool, Ordering}, | 7 | atomic::{AtomicBool, AtomicU64, Ordering}, |
| 8 | mpsc::{self, SyncSender}, | 8 | mpsc::{self, SyncSender}, |
| 9 | }, | 9 | }, |
| 10 | thread::{self, JoinHandle}, | 10 | thread::{self, JoinHandle}, |
| ... | @@ -13,6 +13,11 @@ use std::{ | ... | @@ -13,6 +13,11 @@ use std::{ |
| 13 | 13 | ||
| 14 | pub(super) struct Signal { | 14 | pub(super) struct Signal { |
| 15 | stopped: AtomicBool, | 15 | stopped: AtomicBool, |
| 16 | offline: AtomicBool, | ||
| 17 | /// FILETIME of the last step or poll that reached the remote; 0 before one has. | ||
| 18 | synced: AtomicU64, | ||
| 19 | /// Asked for by `SyncWorker::wake`: steps run while offline until one leaves nothing to do. | ||
| 20 | requested: AtomicBool, | ||
| 16 | sender: SyncSender<()>, | 21 | sender: SyncSender<()>, |
| 17 | } | 22 | } |
| 18 | 23 | ||
| ... | @@ -31,9 +36,23 @@ pub struct SyncWorker { | ... | @@ -31,9 +36,23 @@ pub struct SyncWorker { |
| 31 | } | 36 | } |
| 32 | 37 | ||
| 33 | impl SyncWorker { | 38 | impl SyncWorker { |
| 34 | /// Requests a retry, for example after a network reachability change. | 39 | /// Requests a retry, for example after a network reachability change. Working |
| 40 | /// offline, it synchronizes once, as OneNote's Sync Now does. | ||
| 35 | /// A pending contention backoff finishes before processing the notification. | 41 | /// A pending contention backoff finishes before processing the notification. |
| 36 | pub fn wake(&self) { | 42 | pub fn wake(&self) { |
| 43 | self.signal.requested.store(true, Ordering::Release); | ||
| 44 | self.signal.wake(); | ||
| 45 | } | ||
| 46 | |||
| 47 | /// When a step or poll last reached the remote. | ||
| 48 | pub fn synced(&self) -> Option<u64> { | ||
| 49 | Some(self.signal.synced.load(Ordering::Acquire)).filter(|time| *time != 0) | ||
| 50 | } | ||
| 51 | |||
| 52 | /// Working offline, the worker neither connects nor steps: local edits stay queued | ||
| 53 | /// until `wake`, or until working online again. | ||
| 54 | pub fn set_offline(&self, offline: bool) { | ||
| 55 | self.signal.offline.store(offline, Ordering::Release); | ||
| 37 | self.signal.wake(); | 56 | self.signal.wake(); |
| 38 | } | 57 | } |
| 39 | 58 | ||
| ... | @@ -94,6 +113,9 @@ impl Replica { | ... | @@ -94,6 +113,9 @@ impl Replica { |
| 94 | let (sender, receiver) = mpsc::sync_channel(1); | 113 | let (sender, receiver) = mpsc::sync_channel(1); |
| 95 | let signal = Arc::new(Signal { | 114 | let signal = Arc::new(Signal { |
| 96 | stopped: AtomicBool::new(false), | 115 | stopped: AtomicBool::new(false), |
| 116 | offline: AtomicBool::new(false), | ||
| 117 | synced: AtomicU64::new(0), | ||
| 118 | requested: AtomicBool::new(false), | ||
| 97 | sender, | 119 | sender, |
| 98 | }); | 120 | }); |
| 99 | let replica = Arc::clone(self); | 121 | let replica = Arc::clone(self); |
| ... | @@ -108,9 +130,19 @@ impl Replica { | ... | @@ -108,9 +130,19 @@ impl Replica { |
| 108 | // hears that the remote is reachable and what state the queue is in. | 130 | // hears that the remote is reachable and what state the queue is in. |
| 109 | let mut reported = false; | 131 | let mut reported = false; |
| 110 | while !worker_signal.stopped.load(Ordering::Acquire) { | 132 | while !worker_signal.stopped.load(Ordering::Acquire) { |
| 133 | if worker_signal.offline.load(Ordering::Acquire) | ||
| 134 | && !worker_signal.requested.load(Ordering::Acquire) | ||
| 135 | { | ||
| 136 | remote = None; | ||
| 137 | reported = false; | ||
| 138 | let _ = receiver.recv(); | ||
| 139 | continue; | ||
| 140 | } | ||
| 111 | let result = match remote.as_mut() { | 141 | let result = match remote.as_mut() { |
| 112 | Some(remote) => { | 142 | Some(remote) => { |
| 113 | if reported && replica.settled(remote).unwrap_or(false) { | 143 | if reported && replica.settled(remote).unwrap_or(false) { |
| 144 | worker_signal.requested.store(false, Ordering::Release); | ||
| 145 | worker_signal.synced.store(crate::now(), Ordering::Release); | ||
| 114 | let _ = receiver.recv_timeout(interval); | 146 | let _ = receiver.recv_timeout(interval); |
| 115 | continue; | 147 | continue; |
| 116 | } | 148 | } |
| ... | @@ -126,6 +158,9 @@ impl Replica { | ... | @@ -126,6 +158,9 @@ impl Replica { |
| 126 | Err(error) => Err(Error::RemoteIo(error)), | 158 | Err(error) => Err(Error::RemoteIo(error)), |
| 127 | }, | 159 | }, |
| 128 | }; | 160 | }; |
| 161 | if result.is_ok() { | ||
| 162 | worker_signal.synced.store(crate::now(), Ordering::Release); | ||
| 163 | } | ||
| 129 | observe(&result); | 164 | observe(&result); |
| 130 | match result { | 165 | match result { |
| 131 | Ok(Synced { | 166 | Ok(Synced { |
| ... | @@ -168,6 +203,7 @@ impl Replica { | ... | @@ -168,6 +203,7 @@ impl Replica { |
| 168 | Err(Error::Io(ref error)) if error.kind() == io::ErrorKind::WouldBlock => {} | 203 | Err(Error::Io(ref error)) if error.kind() == io::ErrorKind::WouldBlock => {} |
| 169 | Err(error) => return Err(error), | 204 | Err(error) => return Err(error), |
| 170 | } | 205 | } |
| 206 | worker_signal.requested.store(false, Ordering::Release); | ||
| 171 | if !worker_signal.stopped.load(Ordering::Acquire) { | 207 | if !worker_signal.stopped.load(Ordering::Acquire) { |
| 172 | let _ = receiver.recv_timeout(interval); | 208 | let _ = receiver.recv_timeout(interval); |
| 173 | } | 209 | } |
crates/notebook/tests/session.rs+83-1| ... | @@ -2,7 +2,7 @@ | ... | @@ -2,7 +2,7 @@ |
| 2 | mod ops; | 2 | mod ops; |
| 3 | use notebook::{ | 3 | use notebook::{ |
| 4 | EditStatus, Recovery, Resolution, | 4 | EditStatus, Recovery, Resolution, |
| 5 | session::{Event, Notebook, Section}, | 5 | session::{Event, Notebook, Section, SyncStatus}, |
| 6 | }; | 6 | }; |
| 7 | use onestore::{ | 7 | use onestore::{ |
| 8 | ExGuid, | 8 | ExGuid, |
| ... | @@ -99,6 +99,18 @@ fn published(section: &Section, id: u64) { | ... | @@ -99,6 +99,18 @@ fn published(section: &Section, id: u64) { |
| 99 | } | 99 | } |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | fn status_until(section: &Section, accept: impl Fn(&SyncStatus) -> bool) -> SyncStatus { | ||
| 103 | let deadline = Instant::now() + Duration::from_secs(20); | ||
| 104 | loop { | ||
| 105 | let status = section.sync_status().unwrap(); | ||
| 106 | if accept(&status) { | ||
| 107 | return status; | ||
| 108 | } | ||
| 109 | assert!(Instant::now() < deadline, "the status stayed {status:?}"); | ||
| 110 | std::thread::sleep(Duration::from_millis(20)); | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 102 | fn stored_page(file: &Path, space: ExGuid) -> Page { | 114 | fn stored_page(file: &Path, space: ExGuid) -> Page { |
| 103 | model_ops::page_of(&onestore::read_file(file).unwrap(), space) | 115 | model_ops::page_of(&onestore::read_file(file).unwrap(), space) |
| 104 | } | 116 | } |
| ... | @@ -917,3 +929,73 @@ fn page_versions_restore_and_delete_through_the_session() { | ... | @@ -917,3 +929,73 @@ fn page_versions_restore_and_delete_through_the_session() { |
| 917 | assert_eq!(page.objects, old.objects); | 929 | assert_eq!(page.objects, old.objects); |
| 918 | section.close().unwrap(); | 930 | section.close().unwrap(); |
| 919 | } | 931 | } |
| 932 | |||
| 933 | #[test] | ||
| 934 | fn working_offline_queues_edits_until_sync_now_or_working_online() { | ||
| 935 | let directory = tempfile::tempdir().unwrap(); | ||
| 936 | let file = directory.path().join("notes.one"); | ||
| 937 | let cache = directory.path().join("cache"); | ||
| 938 | std::fs::write( | ||
| 939 | &file, | ||
| 940 | onestore::create_section("notes.one", "Original", "Author").unwrap(), | ||
| 941 | ) | ||
| 942 | .unwrap(); | ||
| 943 | let (section, _) = open(&file, &cache); | ||
| 944 | let space = section.pages().unwrap()[0].0; | ||
| 945 | let before = section.page(space).unwrap(); | ||
| 946 | let reached = status_until(&section, |status| status.synced.is_some()); | ||
| 947 | assert!(reached.error.is_none()); | ||
| 948 | assert_eq!(reached.queued, 0); | ||
| 949 | |||
| 950 | section.set_offline(true); | ||
| 951 | let id = typed(&section, space, &before, 0..0, "Offline "); | ||
| 952 | std::thread::sleep(Duration::from_millis(500)); | ||
| 953 | assert_eq!(section.status(id).unwrap(), Some(EditStatus::Pending)); | ||
| 954 | assert_eq!(section.sync_status().unwrap().queued, 1); | ||
| 955 | assert_eq!(stored_page(&file, space), before); | ||
| 956 | |||
| 957 | // Sync Now publishes while working offline. | ||
| 958 | section.wake(); | ||
| 959 | published(&section, id); | ||
| 960 | let after = edited(&before, "Offline "); | ||
| 961 | assert_same(stored_page(&file, space), &after); | ||
| 962 | let synced = status_until(&section, |status| status.queued == 0); | ||
| 963 | assert!(synced.synced > reached.synced); | ||
| 964 | |||
| 965 | let next = typed(&section, space, &after, 0..0, "Again "); | ||
| 966 | std::thread::sleep(Duration::from_millis(500)); | ||
| 967 | assert_eq!(section.status(next).unwrap(), Some(EditStatus::Pending)); | ||
| 968 | section.set_offline(false); | ||
| 969 | published(&section, next); | ||
| 970 | status_until(&section, |status| status.queued == 0); | ||
| 971 | section.close().unwrap(); | ||
| 972 | } | ||
| 973 | |||
| 974 | #[test] | ||
| 975 | fn a_missing_section_file_reports_its_error_until_it_returns() { | ||
| 976 | let directory = tempfile::tempdir().unwrap(); | ||
| 977 | let file = directory.path().join("notes.one"); | ||
| 978 | let away = directory.path().join("away.one"); | ||
| 979 | let cache = directory.path().join("cache"); | ||
| 980 | std::fs::write( | ||
| 981 | &file, | ||
| 982 | onestore::create_section("notes.one", "Original", "Author").unwrap(), | ||
| 983 | ) | ||
| 984 | .unwrap(); | ||
| 985 | let (section, notified) = open(&file, &cache); | ||
| 986 | status_until(&section, |status| status.synced.is_some()); | ||
| 987 | std::fs::rename(&file, &away).unwrap(); | ||
| 988 | section.wake(); | ||
| 989 | let failed = status_until(&section, |status| status.error.is_some()); | ||
| 990 | assert_eq!( | ||
| 991 | failed.error.map(|error| error.kind()), | ||
| 992 | Some(std::io::ErrorKind::NotFound) | ||
| 993 | ); | ||
| 994 | let before = notified.load(Ordering::SeqCst); | ||
| 995 | std::fs::rename(&away, &file).unwrap(); | ||
| 996 | section.wake(); | ||
| 997 | let back = status_until(&section, |status| status.error.is_none()); | ||
| 998 | assert!(back.synced > failed.synced); | ||
| 999 | assert!(notified.load(Ordering::SeqCst) > before); | ||
| 1000 | section.close().unwrap(); | ||
| 1001 | } |
crates/snowbound/Cargo.toml+7-2| ... | @@ -4,9 +4,14 @@ version = "0.1.0" | ... | @@ -4,9 +4,14 @@ version = "0.1.0" |
| 4 | edition = "2024" | 4 | edition = "2024" |
| 5 | publish = false | 5 | publish = false |
| 6 | 6 | ||
| 7 | [features] | ||
| 8 | default = ["wgpu"] | ||
| 9 | # Paints through wgpu; without it through OpenGL 2.1, for Mac OS X 10.6. | ||
| 10 | wgpu = ["draw/wgpu", "dep:wgpu"] | ||
| 11 | |||
| 7 | [dependencies] | 12 | [dependencies] |
| 8 | canvas = { path = "../canvas", features = ["interaction"] } | 13 | canvas = { path = "../canvas", features = ["interaction"] } |
| 9 | draw = { path = "../draw" } | 14 | draw = { path = "../draw", default-features = false, features = ["render"] } |
| 10 | ui = { path = "../ui" } | 15 | ui = { path = "../ui" } |
| 11 | png = "0.18" | 16 | png = "0.18" |
| 12 | onestore = { path = "../onestore" } | 17 | onestore = { path = "../onestore" } |
| ... | @@ -14,7 +19,7 @@ notebook = { path = "../notebook", features = ["smb"] } | ... | @@ -14,7 +19,7 @@ notebook = { path = "../notebook", features = ["smb"] } |
| 14 | serde = { version = "1", features = ["derive"] } | 19 | serde = { version = "1", features = ["derive"] } |
| 15 | serde_json = "1" | 20 | serde_json = "1" |
| 16 | pollster = "0.4" | 21 | pollster = "0.4" |
| 17 | wgpu.workspace = true | 22 | wgpu = { workspace = true, optional = true } |
| 18 | parley.workspace = true | 23 | parley.workspace = true |
| 19 | winit = "0.30.13" | 24 | winit = "0.30.13" |
| 20 | arboard = { version = "3.6.1", default-features = false } | 25 | arboard = { version = "3.6.1", default-features = false } |
crates/snowbound/src/art.rs-19| ... | @@ -46,41 +46,22 @@ pub const UNDO: &[&str] = art!("icons/undo"); | ... | @@ -46,41 +46,22 @@ pub const UNDO: &[&str] = art!("icons/undo"); |
| 46 | pub const ZOOM_IN: &[&str] = art!("icons/zoom-in"); | 46 | pub const ZOOM_IN: &[&str] = art!("icons/zoom-in"); |
| 47 | pub const ZOOM_OUT: &[&str] = art!("icons/zoom-out"); | 47 | pub const ZOOM_OUT: &[&str] = art!("icons/zoom-out"); |
| 48 | 48 | ||
| 49 | // The toolbar layout's new buttons (resources/toolbar-spec.md); each loses its `expect` | ||
| 50 | // when its button lands. | ||
| 51 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 52 | pub const BACK: &[&str] = art!("icons/back"); | 49 | pub const BACK: &[&str] = art!("icons/back"); |
| 53 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 54 | pub const COPY: &[&str] = art!("icons/copy"); | 50 | pub const COPY: &[&str] = art!("icons/copy"); |
| 55 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 56 | pub const CUT: &[&str] = art!("icons/cut"); | 51 | pub const CUT: &[&str] = art!("icons/cut"); |
| 57 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 58 | pub const DATE_TIME: &[&str] = art!("icons/date-time"); | 52 | pub const DATE_TIME: &[&str] = art!("icons/date-time"); |
| 59 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 60 | pub const FIND_TAGS: &[&str] = art!("icons/find-tags"); | 53 | pub const FIND_TAGS: &[&str] = art!("icons/find-tags"); |
| 61 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 62 | pub const FORMAT_PAINTER: &[&str] = art!("icons/format-painter"); | 54 | pub const FORMAT_PAINTER: &[&str] = art!("icons/format-painter"); |
| 63 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 64 | pub const FORWARD: &[&str] = art!("icons/forward"); | 55 | pub const FORWARD: &[&str] = art!("icons/forward"); |
| 65 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 66 | pub const FULL_PAGE_VIEW: &[&str] = art!("icons/full-page-view"); | 56 | pub const FULL_PAGE_VIEW: &[&str] = art!("icons/full-page-view"); |
| 67 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 68 | pub const INSERT_SPACE: &[&str] = art!("icons/insert-space"); | 57 | pub const INSERT_SPACE: &[&str] = art!("icons/insert-space"); |
| 69 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 70 | pub const PAGE_COLOR: &[&str] = art!("icons/page-color"); | 58 | pub const PAGE_COLOR: &[&str] = art!("icons/page-color"); |
| 71 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 72 | pub const PASTE: &[&str] = art!("icons/paste"); | 59 | pub const PASTE: &[&str] = art!("icons/paste"); |
| 73 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 74 | pub const RECORD_AUDIO: &[&str] = art!("icons/record-audio"); | 60 | pub const RECORD_AUDIO: &[&str] = art!("icons/record-audio"); |
| 75 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 76 | pub const RECORD_VIDEO: &[&str] = art!("icons/record-video"); | 61 | pub const RECORD_VIDEO: &[&str] = art!("icons/record-video"); |
| 77 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 78 | pub const RULE_LINES: &[&str] = art!("icons/rule-lines"); | 62 | pub const RULE_LINES: &[&str] = art!("icons/rule-lines"); |
| 79 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 80 | pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping"); | 63 | pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping"); |
| 81 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 82 | pub const SPELLING: &[&str] = art!("icons/spelling"); | 64 | pub const SPELLING: &[&str] = art!("icons/spelling"); |
| 83 | #[expect(dead_code, reason = "Its toolbar button has not landed")] | ||
| 84 | pub const SYMBOL: &[&str] = art!("icons/symbol"); | 65 | pub const SYMBOL: &[&str] = art!("icons/symbol"); |
| 85 | 66 | ||
| 86 | pub const TAG_REMEMBER: &[&str] = art!("tags/remember"); | 67 | pub const TAG_REMEMBER: &[&str] = art!("tags/remember"); |
crates/snowbound/src/commands.rs+139-5| ... | @@ -25,6 +25,7 @@ pub enum Id { | ... | @@ -25,6 +25,7 @@ pub enum Id { |
| 25 | Cut, | 25 | Cut, |
| 26 | Copy, | 26 | Copy, |
| 27 | Paste, | 27 | Paste, |
| 28 | FormatPainter, | ||
| 28 | SelectAll, | 29 | SelectAll, |
| 29 | Find, | 30 | Find, |
| 30 | Search, | 31 | Search, |
| ... | @@ -36,14 +37,21 @@ pub enum Id { | ... | @@ -36,14 +37,21 @@ pub enum Id { |
| 36 | Sidebar, | 37 | Sidebar, |
| 37 | PageList, | 38 | PageList, |
| 38 | DarkPages, | 39 | DarkPages, |
| 40 | FullPageView, | ||
| 41 | PageColor, | ||
| 39 | Table, | 42 | Table, |
| 40 | Picture, | 43 | Picture, |
| 44 | ScreenClipping, | ||
| 41 | Attachment, | 45 | Attachment, |
| 42 | Link, | 46 | Link, |
| 47 | InsertSpace, | ||
| 43 | Equation, | 48 | Equation, |
| 49 | Symbol, | ||
| 44 | Date, | 50 | Date, |
| 45 | Time, | 51 | Time, |
| 46 | DateTime, | 52 | DateTime, |
| 53 | RecordAudio, | ||
| 54 | RecordVideo, | ||
| 47 | Toggle(Toggle), | 55 | Toggle(Toggle), |
| 48 | Highlight, | 56 | Highlight, |
| 49 | FontColor, | 57 | FontColor, |
| ... | @@ -69,6 +77,10 @@ pub enum Choice { | ... | @@ -69,6 +77,10 @@ pub enum Choice { |
| 69 | /// A COLORREF, or `None` for automatic. | 77 | /// A COLORREF, or `None` for automatic. |
| 70 | Color(Option<u32>), | 78 | Color(Option<u32>), |
| 71 | List(Option<ListStyle>), | 79 | List(Option<ListStyle>), |
| 80 | /// An empty table of rows by columns. | ||
| 81 | Table([usize; 2]), | ||
| 82 | /// An index into `PAGE_COLORS`, or `None` for no colour. | ||
| 83 | PageColor(Option<usize>), | ||
| 72 | } | 84 | } |
| 73 | 85 | ||
| 74 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] | 86 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| ... | @@ -161,6 +173,9 @@ macro_rules! tag { | ... | @@ -161,6 +173,9 @@ macro_rules! tag { |
| 161 | ($tag:expr, $digit:literal) => { | 173 | ($tag:expr, $digit:literal) => { |
| 162 | row!(Id::Tag($tag), $tag.label(), &[cmd($digit)], &[cmd($digit)]) | 174 | row!(Id::Tag($tag), $tag.label(), &[cmd($digit)], &[cmd($digit)]) |
| 163 | }; | 175 | }; |
| 176 | ($tag:expr) => { | ||
| 177 | row!(Id::Tag($tag), $tag.label(), NONE, NONE) | ||
| 178 | }; | ||
| 164 | } | 179 | } |
| 165 | 180 | ||
| 166 | const NONE: &[Chord] = &[]; | 181 | const NONE: &[Chord] = &[]; |
| ... | @@ -206,6 +221,7 @@ pub const COMMANDS: &[Command] = &[ | ... | @@ -206,6 +221,7 @@ pub const COMMANDS: &[Command] = &[ |
| 206 | row!(Id::Cut, "Cut", &[cmd('x')], &[cmd('x')]), | 221 | row!(Id::Cut, "Cut", &[cmd('x')], &[cmd('x')]), |
| 207 | row!(Id::Copy, "Copy", &[cmd('c')], &[cmd('c')]), | 222 | row!(Id::Copy, "Copy", &[cmd('c')], &[cmd('c')]), |
| 208 | row!(Id::Paste, "Paste", &[cmd('v')], &[cmd('v')]), | 223 | row!(Id::Paste, "Paste", &[cmd('v')], &[cmd('v')]), |
| 224 | row!(Id::FormatPainter, "Format Painter", NONE, NONE), | ||
| 209 | row!(Id::SelectAll, "Select All", &[cmd('a')], &[cmd('a')]), | 225 | row!(Id::SelectAll, "Select All", &[cmd('a')], &[cmd('a')]), |
| 210 | row!(Id::Find, "Find on This Page", &[cmd('f')], &[cmd('f')]), | 226 | row!(Id::Find, "Find on This Page", &[cmd('f')], &[cmd('f')]), |
| 211 | row!(Id::Search, "Search Notebooks", &[cmd('e')], &[cmd('e')]), | 227 | row!(Id::Search, "Search Notebooks", &[cmd('e')], &[cmd('e')]), |
| ... | @@ -239,16 +255,27 @@ pub const COMMANDS: &[Command] = &[ | ... | @@ -239,16 +255,27 @@ pub const COMMANDS: &[Command] = &[ |
| 239 | row!(Id::Sidebar, "Sidebar", &[cmd('s').control()], NONE), | 255 | row!(Id::Sidebar, "Sidebar", &[cmd('s').control()], NONE), |
| 240 | row!(Id::PageList, "Page List", NONE, NONE), | 256 | row!(Id::PageList, "Page List", NONE, NONE), |
| 241 | row!(Id::DarkPages, "Dark Pages", NONE, NONE), | 257 | row!(Id::DarkPages, "Dark Pages", NONE, NONE), |
| 258 | // AppKit's Enter Full Screen takes Control-Command-F. | ||
| 259 | row!( | ||
| 260 | Id::FullPageView, | ||
| 261 | "Full Page View", | ||
| 262 | NONE, | ||
| 263 | &[named(NamedKey::F11)] | ||
| 264 | ), | ||
| 265 | row!(Id::PageColor, "Page Color", NONE, NONE), | ||
| 242 | row!(Id::Table, "Table", NONE, NONE), | 266 | row!(Id::Table, "Table", NONE, NONE), |
| 243 | row!(Id::Picture, "Picture…", NONE, NONE), | 267 | row!(Id::Picture, "Picture…", NONE, NONE), |
| 268 | row!(Id::ScreenClipping, "Screen Clipping", NONE, NONE), | ||
| 244 | row!(Id::Attachment, "Attach File…", NONE, NONE), | 269 | row!(Id::Attachment, "Attach File…", NONE, NONE), |
| 245 | row!(Id::Link, "Link…", &[cmd('k')], &[cmd('k')]), | 270 | row!(Id::Link, "Link…", &[cmd('k')], &[cmd('k')]), |
| 271 | row!(Id::InsertSpace, "Insert Space", NONE, NONE), | ||
| 246 | row!( | 272 | row!( |
| 247 | Id::Equation, | 273 | Id::Equation, |
| 248 | "Equation", | 274 | "Equation", |
| 249 | &[key('=').control()], | 275 | &[key('=').control()], |
| 250 | &[key('=').option()], | 276 | &[key('=').option()], |
| 251 | ), | 277 | ), |
| 278 | row!(Id::Symbol, "Symbol", NONE, NONE), | ||
| 252 | row!(Id::Date, "Date", NONE, &[key('d').option().shift()]), | 279 | row!(Id::Date, "Date", NONE, &[key('d').option().shift()]), |
| 253 | row!(Id::Time, "Time", NONE, &[key('t').option().shift()]), | 280 | row!(Id::Time, "Time", NONE, &[key('t').option().shift()]), |
| 254 | row!( | 281 | row!( |
| ... | @@ -257,6 +284,8 @@ pub const COMMANDS: &[Command] = &[ | ... | @@ -257,6 +284,8 @@ pub const COMMANDS: &[Command] = &[ |
| 257 | NONE, | 284 | NONE, |
| 258 | &[key('f').option().shift()] | 285 | &[key('f').option().shift()] |
| 259 | ), | 286 | ), |
| 287 | row!(Id::RecordAudio, "Record Audio", NONE, NONE), | ||
| 288 | row!(Id::RecordVideo, "Record Video", NONE, NONE), | ||
| 260 | row!(Id::Toggle(Toggle::Bold), "Bold", &[cmd('b')], &[cmd('b')]), | 289 | row!(Id::Toggle(Toggle::Bold), "Bold", &[cmd('b')], &[cmd('b')]), |
| 261 | row!( | 290 | row!( |
| 262 | Id::Toggle(Toggle::Italic), | 291 | Id::Toggle(Toggle::Italic), |
| ... | @@ -338,6 +367,26 @@ pub const COMMANDS: &[Command] = &[ | ... | @@ -338,6 +367,26 @@ pub const COMMANDS: &[Command] = &[ |
| 338 | tag!(NoteTag::Contact, '7'), | 367 | tag!(NoteTag::Contact, '7'), |
| 339 | tag!(NoteTag::Address, '8'), | 368 | tag!(NoteTag::Address, '8'), |
| 340 | tag!(NoteTag::PhoneNumber, '9'), | 369 | tag!(NoteTag::PhoneNumber, '9'), |
| 370 | tag!(NoteTag::WebSiteToVisit), | ||
| 371 | tag!(NoteTag::Idea), | ||
| 372 | tag!(NoteTag::Password), | ||
| 373 | tag!(NoteTag::Critical), | ||
| 374 | tag!(NoteTag::ProjectA), | ||
| 375 | tag!(NoteTag::ProjectB), | ||
| 376 | tag!(NoteTag::MovieToSee), | ||
| 377 | tag!(NoteTag::BookToRead), | ||
| 378 | tag!(NoteTag::MusicToListenTo), | ||
| 379 | tag!(NoteTag::SourceForArticle), | ||
| 380 | tag!(NoteTag::RememberForBlog), | ||
| 381 | tag!(NoteTag::DiscussWithPersonA), | ||
| 382 | tag!(NoteTag::DiscussWithPersonB), | ||
| 383 | tag!(NoteTag::DiscussWithManager), | ||
| 384 | tag!(NoteTag::SendInEmail), | ||
| 385 | tag!(NoteTag::ScheduleMeeting), | ||
| 386 | tag!(NoteTag::CallBack), | ||
| 387 | tag!(NoteTag::ToDoPriority1), | ||
| 388 | tag!(NoteTag::ToDoPriority2), | ||
| 389 | tag!(NoteTag::ClientRequest), | ||
| 341 | row!( | 390 | row!( |
| 342 | Id::RemoveTags, | 391 | Id::RemoveTags, |
| 343 | "Remove Tag", | 392 | "Remove Tag", |
| ... | @@ -393,7 +442,9 @@ pub fn title(choice: Choice) -> String { | ... | @@ -393,7 +442,9 @@ pub fn title(choice: Choice) -> String { |
| 393 | .iter() | 442 | .iter() |
| 394 | .find(|(listed, _)| *listed == color) | 443 | .find(|(listed, _)| *listed == color) |
| 395 | .map_or_else(String::new, |(_, name)| (*name).to_owned()), | 444 | .map_or_else(String::new, |(_, name)| (*name).to_owned()), |
| 396 | Choice::Color(_) | Choice::List(_) => unreachable!("Only the toolbar offers these"), | 445 | Choice::Color(_) | Choice::List(_) | Choice::Table(_) | Choice::PageColor(_) => { |
| 446 | unreachable!("Only the toolbar offers these") | ||
| 447 | } | ||
| 397 | } | 448 | } |
| 398 | } | 449 | } |
| 399 | 450 | ||
| ... | @@ -574,6 +625,8 @@ impl State { | ... | @@ -574,6 +625,8 @@ impl State { |
| 574 | Choice::Font(name) => return checked(format.font.as_ref() == Some(name)), | 625 | Choice::Font(name) => return checked(format.font.as_ref() == Some(name)), |
| 575 | Choice::Size(size) => return checked(format.font_size == Some(*size)), | 626 | Choice::Size(size) => return checked(format.font_size == Some(*size)), |
| 576 | Choice::Highlight(_) | Choice::Color(_) | Choice::List(_) => return enabled(text), | 627 | Choice::Highlight(_) | Choice::Color(_) | Choice::List(_) => return enabled(text), |
| 628 | Choice::Table(_) => Id::Table, | ||
| 629 | Choice::PageColor(_) => Id::PageColor, | ||
| 577 | }; | 630 | }; |
| 578 | match id { | 631 | match id { |
| 579 | Id::Settings | Id::NewNotebook | Id::OpenNotebook | Id::Help => enabled(!modal), | 632 | Id::Settings | Id::NewNotebook | Id::OpenNotebook | Id::Help => enabled(!modal), |
| ... | @@ -595,6 +648,10 @@ impl State { | ... | @@ -595,6 +648,10 @@ impl State { |
| 595 | checked: session.shown_history == Some(session.space), | 648 | checked: session.shown_history == Some(session.space), |
| 596 | }) | 649 | }) |
| 597 | } | 650 | } |
| 651 | Id::FullPageView => Status { | ||
| 652 | enabled: !welcome && !modal, | ||
| 653 | checked: self.full_page, | ||
| 654 | }, | ||
| 598 | Id::Undo => enabled(writable && !field && self.view.editor.can_undo()), | 655 | Id::Undo => enabled(writable && !field && self.view.editor.can_undo()), |
| 599 | Id::Redo => enabled(writable && !field && self.view.editor.can_redo()), | 656 | Id::Redo => enabled(writable && !field && self.view.editor.can_redo()), |
| 600 | Id::Cut => enabled(writable && selected), | 657 | Id::Cut => enabled(writable && selected), |
| ... | @@ -616,8 +673,18 @@ impl State { | ... | @@ -616,8 +673,18 @@ impl State { |
| 616 | enabled: !modal, | 673 | enabled: !modal, |
| 617 | checked: !self.light_pages, | 674 | checked: !self.light_pages, |
| 618 | }, | 675 | }, |
| 619 | Id::Table | Id::Picture | Id::Attachment => enabled(false), | 676 | Id::FormatPainter => Status { |
| 620 | Id::Link | 677 | enabled: text, |
| 678 | checked: self.painter.is_some(), | ||
| 679 | }, | ||
| 680 | Id::PageColor => enabled(writable && session.is_some()), | ||
| 681 | Id::ScreenClipping => enabled(text && cfg!(target_os = "macos")), | ||
| 682 | // Attach File awaits #23, Record Audio and Video #36, and Insert Space a drag mode. | ||
| 683 | Id::Attachment | Id::RecordAudio | Id::RecordVideo | Id::InsertSpace => enabled(false), | ||
| 684 | Id::Table | ||
| 685 | | Id::Picture | ||
| 686 | | Id::Symbol | ||
| 687 | | Id::Link | ||
| 621 | | Id::Equation | 688 | | Id::Equation |
| 622 | | Id::Date | 689 | | Id::Date |
| 623 | | Id::Time | 690 | | Id::Time |
| ... | @@ -638,7 +705,7 @@ impl State { | ... | @@ -638,7 +705,7 @@ impl State { |
| 638 | 705 | ||
| 639 | /// Applies `formatting`, remembering its font, colour or list style as the toolbar's | 706 | /// Applies `formatting`, remembering its font, colour or list style as the toolbar's |
| 640 | /// last pick. | 707 | /// last pick. |
| 641 | fn format(&mut self, formatting: Formatting) -> Result<(), Box<dyn Error>> { | 708 | pub(crate) fn format(&mut self, formatting: Formatting) -> Result<(), Box<dyn Error>> { |
| 642 | let kept = self.toolbar.clone(); | 709 | let kept = self.toolbar.clone(); |
| 643 | let pens = &mut self.toolbar; | 710 | let pens = &mut self.toolbar; |
| 644 | match &formatting { | 711 | match &formatting { |
| ... | @@ -661,6 +728,26 @@ impl State { | ... | @@ -661,6 +728,26 @@ impl State { |
| 661 | Ok(()) | 728 | Ok(()) |
| 662 | } | 729 | } |
| 663 | 730 | ||
| 731 | /// Puts an encoded picture after the caret's paragraph at its size on a 96 dpi screen, | ||
| 732 | /// as OneNote 2010 inserts one. | ||
| 733 | pub(crate) fn insert_picture(&mut self, bytes: Vec<u8>) -> Result<(), Box<dyn Error>> { | ||
| 734 | let pixels = match draw::RasterImage::measure(&bytes) { | ||
| 735 | Ok(pixels) => pixels, | ||
| 736 | Err(_) => { | ||
| 737 | platform::alert( | ||
| 738 | "Couldn't insert the picture", | ||
| 739 | "Choose a PNG, JPEG or GIF picture.", | ||
| 740 | ); | ||
| 741 | return Ok(()); | ||
| 742 | } | ||
| 743 | }; | ||
| 744 | let response = self | ||
| 745 | .view | ||
| 746 | .insert_picture(bytes, pixels.map(|side| side as f32 * 0.75))?; | ||
| 747 | self.respond(response); | ||
| 748 | Ok(()) | ||
| 749 | } | ||
| 750 | |||
| 664 | /// The notebook the open section, or the notebook showing none, belongs to. | 751 | /// The notebook the open section, or the notebook showing none, belongs to. |
| 665 | fn notebook(&self) -> Option<&Arc<crate::Library>> { | 752 | fn notebook(&self) -> Option<&Arc<crate::Library>> { |
| 666 | self.session | 753 | self.session |
| ... | @@ -682,6 +769,15 @@ impl State { | ... | @@ -682,6 +769,15 @@ impl State { |
| 682 | Choice::Highlight(color) => return format(self, Formatting::Highlight(color)), | 769 | Choice::Highlight(color) => return format(self, Formatting::Highlight(color)), |
| 683 | Choice::Color(color) => return format(self, Formatting::Color(color)), | 770 | Choice::Color(color) => return format(self, Formatting::Color(color)), |
| 684 | Choice::List(style) => return format(self, Formatting::List(style)), | 771 | Choice::List(style) => return format(self, Formatting::List(style)), |
| 772 | Choice::Table([rows, columns]) => { | ||
| 773 | let response = self.view.insert_table(rows, columns)?; | ||
| 774 | self.respond(response); | ||
| 775 | return Ok(()); | ||
| 776 | } | ||
| 777 | Choice::PageColor(color) => { | ||
| 778 | let color = color.map(|index| canvas::template::PAGE_COLORS[index].1); | ||
| 779 | return self.color_page(color); | ||
| 780 | } | ||
| 685 | }; | 781 | }; |
| 686 | let field = self.ui.focused().filter(|focus| *focus != page()); | 782 | let field = self.ui.focused().filter(|focus| *focus != page()); |
| 687 | let response = match id { | 783 | let response = match id { |
| ... | @@ -740,6 +836,13 @@ impl State { | ... | @@ -740,6 +836,13 @@ impl State { |
| 740 | return Ok(()); | 836 | return Ok(()); |
| 741 | } | 837 | } |
| 742 | Id::Paste => Work::Page(Request::Paste), | 838 | Id::Paste => Work::Page(Request::Paste), |
| 839 | Id::FormatPainter => { | ||
| 840 | self.painter = match self.painter { | ||
| 841 | Some(_) => None, | ||
| 842 | None => Some(self.view.editor.painted_format()?), | ||
| 843 | }; | ||
| 844 | return Ok(()); | ||
| 845 | } | ||
| 743 | Id::SelectAll => { | 846 | Id::SelectAll => { |
| 744 | match field { | 847 | match field { |
| 745 | Some(field) => self.ui.focus_all(field), | 848 | Some(field) => self.ui.focus_all(field), |
| ... | @@ -779,13 +882,44 @@ impl State { | ... | @@ -779,13 +882,44 @@ impl State { |
| 779 | self.pages_open = !self.pages_open; | 882 | self.pages_open = !self.pages_open; |
| 780 | return Ok(()); | 883 | return Ok(()); |
| 781 | } | 884 | } |
| 885 | Id::FullPageView => { | ||
| 886 | self.full_page = !self.full_page; | ||
| 887 | return Ok(()); | ||
| 888 | } | ||
| 889 | // The menu bar and keys open the toolbar's gallery. | ||
| 890 | Id::Table | Id::PageColor => { | ||
| 891 | let name = if id == Id::Table { | ||
| 892 | "table" | ||
| 893 | } else { | ||
| 894 | "page color" | ||
| 895 | }; | ||
| 896 | self.ui.open_popup(crate::toolbar_popup(name)); | ||
| 897 | return Ok(()); | ||
| 898 | } | ||
| 899 | Id::Picture => { | ||
| 900 | let Some(path) = platform::pick_picture() else { | ||
| 901 | return Ok(()); | ||
| 902 | }; | ||
| 903 | return self.insert_picture(std::fs::read(path)?); | ||
| 904 | } | ||
| 905 | Id::ScreenClipping => { | ||
| 906 | #[cfg(target_os = "macos")] | ||
| 907 | platform::clip_screen(self.proxy.clone()); | ||
| 908 | return Ok(()); | ||
| 909 | } | ||
| 910 | Id::Symbol => { | ||
| 911 | platform::character_palette(); | ||
| 912 | return Ok(()); | ||
| 913 | } | ||
| 782 | Id::DarkPages => { | 914 | Id::DarkPages => { |
| 783 | self.light_pages = !self.light_pages; | 915 | self.light_pages = !self.light_pages; |
| 784 | self.follow_color_scheme(); | 916 | self.follow_color_scheme(); |
| 785 | self.save_settings(); | 917 | self.save_settings(); |
| 786 | return Ok(()); | 918 | return Ok(()); |
| 787 | } | 919 | } |
| 788 | Id::Table | Id::Picture | Id::Attachment => return Ok(()), | 920 | Id::Attachment | Id::RecordAudio | Id::RecordVideo | Id::InsertSpace => { |
| 921 | return Ok(()); | ||
| 922 | } | ||
| 789 | Id::Link => { | 923 | Id::Link => { |
| 790 | self.open_link_dialog(); | 924 | self.open_link_dialog(); |
| 791 | return Ok(()); | 925 | return Ok(()); |
crates/snowbound/src/history.rs-3| ... | @@ -165,7 +165,6 @@ impl State { | ... | @@ -165,7 +165,6 @@ impl State { |
| 165 | session | 165 | session |
| 166 | .section | 166 | .section |
| 167 | .restore_version(page, version, &self.author)?; | 167 | .restore_version(page, version, &self.author)?; |
| 168 | session.status = "Saving"; | ||
| 169 | session.refresh_history()?; | 168 | session.refresh_history()?; |
| 170 | self.commands.push(Command::OpenPage(page)); | 169 | self.commands.push(Command::OpenPage(page)); |
| 171 | Ok(()) | 170 | Ok(()) |
| ... | @@ -178,7 +177,6 @@ impl State { | ... | @@ -178,7 +177,6 @@ impl State { |
| 178 | ) -> Result<(), Box<dyn Error>> { | 177 | ) -> Result<(), Box<dyn Error>> { |
| 179 | let session = self.session.as_mut().ok_or("No section is open")?; | 178 | let session = self.session.as_mut().ok_or("No section is open")?; |
| 180 | session.section.delete_versions(&[(page, vec![version])])?; | 179 | session.section.delete_versions(&[(page, vec![version])])?; |
| 181 | session.status = "Saving"; | ||
| 182 | session.refresh_history()?; | 180 | session.refresh_history()?; |
| 183 | self.commands.push(Command::OpenPage(page)); | 181 | self.commands.push(Command::OpenPage(page)); |
| 184 | Ok(()) | 182 | Ok(()) |
| ... | @@ -221,7 +219,6 @@ impl State { | ... | @@ -221,7 +219,6 @@ impl State { |
| 221 | .collect(); | 219 | .collect(); |
| 222 | if !all.is_empty() { | 220 | if !all.is_empty() { |
| 223 | session.section.delete_versions(&all)?; | 221 | session.section.delete_versions(&all)?; |
| 224 | session.status = "Saving"; | ||
| 225 | } | 222 | } |
| 226 | session.refresh_history()?; | 223 | session.refresh_history()?; |
| 227 | if session.version.is_some() { | 224 | if session.version.is_some() { |
crates/snowbound/src/library.rs+32-3| ... | @@ -7,10 +7,25 @@ use std::{ | ... | @@ -7,10 +7,25 @@ use std::{ |
| 7 | error::Error, | 7 | error::Error, |
| 8 | io, | 8 | io, |
| 9 | path::{Path, PathBuf}, | 9 | path::{Path, PathBuf}, |
| 10 | sync::Arc, | 10 | sync::{ |
| 11 | Arc, | ||
| 12 | atomic::{AtomicBool, Ordering}, | ||
| 13 | }, | ||
| 11 | time::Duration, | 14 | time::Duration, |
| 12 | }; | 15 | }; |
| 13 | 16 | ||
| 17 | /// OneNote's Work Offline, which like OneNote's holds for every notebook. | ||
| 18 | static OFFLINE: AtomicBool = AtomicBool::new(false); | ||
| 19 | |||
| 20 | pub fn offline() -> bool { | ||
| 21 | OFFLINE.load(Ordering::Relaxed) | ||
| 22 | } | ||
| 23 | |||
| 24 | /// Works offline or online again; sections opened from now on follow. | ||
| 25 | pub fn set_offline(offline: bool) { | ||
| 26 | OFFLINE.store(offline, Ordering::Relaxed); | ||
| 27 | } | ||
| 28 | |||
| 14 | /// How long an SMB request may take before the share counts as unreachable. | 29 | /// How long an SMB request may take before the share counts as unreachable. |
| 15 | const TIMEOUT: Duration = Duration::from_secs(10); | 30 | const TIMEOUT: Duration = Duration::from_secs(10); |
| 16 | /// The largest section file read whole over SMB. | 31 | /// The largest section file read whole over SMB. |
| ... | @@ -243,7 +258,7 @@ impl Library { | ... | @@ -243,7 +258,7 @@ impl Library { |
| 243 | path: &str, | 258 | path: &str, |
| 244 | notify: impl Fn() + Send + 'static, | 259 | notify: impl Fn() + Send + 'static, |
| 245 | ) -> Result<Section, Box<dyn Error>> { | 260 | ) -> Result<Section, Box<dyn Error>> { |
| 246 | Ok(match (&self.notebook, &self.server) { | 261 | let section = match (&self.notebook, &self.server) { |
| 247 | (Ok(Some(notebook)), Some(server)) => { | 262 | (Ok(Some(notebook)), Some(server)) => { |
| 248 | let identity = self | 263 | let identity = self |
| 249 | .catalog_section(notebook.catalog(), path) | 264 | .catalog_section(notebook.catalog(), path) |
| ... | @@ -268,7 +283,21 @@ impl Library { | ... | @@ -268,7 +283,21 @@ impl Library { |
| 268 | (Ok(Some(notebook)), None) => notebook.section(path, notify)?, | 283 | (Ok(Some(notebook)), None) => notebook.section(path, notify)?, |
| 269 | (Ok(None), _) => Section::open(path, &self.cache, notify)?, | 284 | (Ok(None), _) => Section::open(path, &self.cache, notify)?, |
| 270 | (Err(error), _) => return Err(error.clone().into()), | 285 | (Err(error), _) => return Err(error.clone().into()), |
| 271 | }) | 286 | }; |
| 287 | section.set_offline(offline()); | ||
| 288 | Ok(section) | ||
| 289 | } | ||
| 290 | |||
| 291 | /// How the notebook's files are reached. | ||
| 292 | pub fn transport(&self) -> String { | ||
| 293 | match (&self.server, &self.notice) { | ||
| 294 | (Some(server), _) => format!( | ||
| 295 | "Snowbound’s SMB client, {}/{}", | ||
| 296 | server.mount.server, server.mount.share | ||
| 297 | ), | ||
| 298 | (None, Some(_)) => "The system’s mount of the share".to_owned(), | ||
| 299 | (None, None) => "Folder on this computer".to_owned(), | ||
| 300 | } | ||
| 272 | } | 301 | } |
| 273 | 302 | ||
| 274 | /// The file identity, in hex, of the section at catalog `path`. | 303 | /// The file identity, in hex, of the section at catalog `path`. |
crates/snowbound/src/linux.rs+277-15| ... | @@ -45,7 +45,7 @@ pub fn event_loop(_headless: bool) -> Result<EventLoop<crate::UserEvent>, EventL | ... | @@ -45,7 +45,7 @@ pub fn event_loop(_headless: bool) -> Result<EventLoop<crate::UserEvent>, EventL |
| 45 | QUIT.set(event_loop.create_proxy()) | 45 | QUIT.set(event_loop.create_proxy()) |
| 46 | .expect("Only one application event loop is created"); | 46 | .expect("Only one application event loop is created"); |
| 47 | watch_settings(event_loop.create_proxy()); | 47 | watch_settings(event_loop.create_proxy()); |
| 48 | if std::env::var("XDG_CURRENT_DESKTOP").is_ok_and(|desktop| desktop.contains("KDE")) { | 48 | if desktop() == Desktop::Kde { |
| 49 | follow_breeze_radius(event_loop.create_proxy()); | 49 | follow_breeze_radius(event_loop.create_proxy()); |
| 50 | } | 50 | } |
| 51 | Ok(event_loop) | 51 | Ok(event_loop) |
| ... | @@ -198,14 +198,10 @@ pub fn install_backdrop(_: &Window) -> bool { | ... | @@ -198,14 +198,10 @@ pub fn install_backdrop(_: &Window) -> bool { |
| 198 | /// suit `appearance`: KWin's from the KDE colour scheme, or on GNOME the Adwaita header bar | 198 | /// suit `appearance`: KWin's from the KDE colour scheme, or on GNOME the Adwaita header bar |
| 199 | /// that winit's frame and GNOME's both draw. | 199 | /// that winit's frame and GNOME's both draw. |
| 200 | pub fn titlebar(appearance: Theme) -> Option<[[f32; 4]; 2]> { | 200 | pub fn titlebar(appearance: Theme) -> Option<[[f32; 4]; 2]> { |
| 201 | let desktops = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); | 201 | let fills = match desktop() { |
| 202 | let on = |name: &str| desktops.split(':').any(|desktop| desktop == name); | 202 | Desktop::Kde => kde_titlebar(&kconfig("kdeglobals")), |
| 203 | let fills = if on("KDE") { | 203 | Desktop::Gnome => adwaita_titlebar(appearance), |
| 204 | kde_titlebar(&kconfig("kdeglobals")) | 204 | Desktop::Other => return None, |
| 205 | } else if on("GNOME") { | ||
| 206 | adwaita_titlebar(appearance) | ||
| 207 | } else { | ||
| 208 | return None; | ||
| 209 | } | 205 | } |
| 210 | .map(|[red, green, blue]| draw::srgb(red, green, blue)); | 206 | .map(|[red, green, blue]| draw::srgb(red, green, blue)); |
| 211 | // Past mid grey, as against a colour scheme chosen in Options, the theme's own strip. | 207 | // Past mid grey, as against a colour scheme chosen in Options, the theme's own strip. |
| ... | @@ -214,6 +210,184 @@ pub fn titlebar(appearance: Theme) -> Option<[[f32; 4]; 2]> { | ... | @@ -214,6 +210,184 @@ pub fn titlebar(appearance: Theme) -> Option<[[f32; 4]; 2]> { |
| 214 | (dark == (appearance == Theme::Dark)).then_some(fills) | 210 | (dark == (appearance == Theme::Dark)).then_some(fills) |
| 215 | } | 211 | } |
| 216 | 212 | ||
| 213 | #[derive(Clone, Copy, Debug, PartialEq)] | ||
| 214 | enum Desktop { | ||
| 215 | Gnome, | ||
| 216 | Kde, | ||
| 217 | Other, | ||
| 218 | } | ||
| 219 | |||
| 220 | /// The desktop the session runs, whose look and motion the chrome follows. | ||
| 221 | fn desktop() -> Desktop { | ||
| 222 | static DESKTOP: OnceLock<Desktop> = OnceLock::new(); | ||
| 223 | *DESKTOP.get_or_init(|| { | ||
| 224 | let desktops = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); | ||
| 225 | let on = |name: &str| desktops.split(':').any(|desktop| desktop == name); | ||
| 226 | if on("KDE") { | ||
| 227 | Desktop::Kde | ||
| 228 | } else if on("GNOME") { | ||
| 229 | Desktop::Gnome | ||
| 230 | } else { | ||
| 231 | Desktop::Other | ||
| 232 | } | ||
| 233 | }) | ||
| 234 | } | ||
| 235 | |||
| 236 | /// Menus in `appearance` as the desktop draws its own: libadwaita's popover menus on GNOME | ||
| 237 | /// and Breeze's on KDE, each moving as that desktop moves them. | ||
| 238 | pub fn menu(appearance: Theme) -> Option<ui::Menu> { | ||
| 239 | match desktop() { | ||
| 240 | Desktop::Gnome => { | ||
| 241 | // GNOME's interface font, as "Family Size". | ||
| 242 | let points = portal_setting("org.gnome.desktop.interface", "font-name") | ||
| 243 | .and_then(|font| String::try_from(font).ok()) | ||
| 244 | .and_then(|font| font.rsplit_once(' ')?.1.parse().ok()); | ||
| 245 | Some(adwaita_menu(appearance, points.unwrap_or(11.0))) | ||
| 246 | } | ||
| 247 | Desktop::Kde => Some(breeze_menu( | ||
| 248 | appearance, | ||
| 249 | &kconfig("kdeglobals"), | ||
| 250 | &kconfig("kwinrc"), | ||
| 251 | )), | ||
| 252 | Desktop::Other => None, | ||
| 253 | } | ||
| 254 | } | ||
| 255 | |||
| 256 | /// libadwaita 1.7's `popover.menu` at `points`, which GTK 4 shows and hides at once. | ||
| 257 | fn adwaita_menu(appearance: Theme, points: f32) -> ui::Menu { | ||
| 258 | let dark = appearance == Theme::Dark; | ||
| 259 | let fill = if dark { [0x36, 0x36, 0x3a] } else { [0xff; 3] }; | ||
| 260 | // `--popover-fg-color`, and `currentColor` mixed with transparency at each strength. | ||
| 261 | let (ink, strength) = if dark { | ||
| 262 | ([0xff; 3], 1.0) | ||
| 263 | } else { | ||
| 264 | ([0, 0, 6], 0.8) | ||
| 265 | }; | ||
| 266 | let ink = |alpha: f32| linear(over(ink, strength * alpha, fill)); | ||
| 267 | ui::Menu { | ||
| 268 | fill: linear(fill), | ||
| 269 | border: linear(over([0; 3], 0.14, fill)), | ||
| 270 | shadows: vec![ | ||
| 271 | css_shadow(0.09, 5.0, 1.0, 1.0), | ||
| 272 | css_shadow(0.05, 14.0, 2.0, 3.0), | ||
| 273 | ], | ||
| 274 | text: ink(1.0), | ||
| 275 | // The accelerator's `--dim-opacity`, and `--disabled-opacity`. | ||
| 276 | dim: ink(0.55), | ||
| 277 | disabled: ink(0.5), | ||
| 278 | highlight: ink(0.1), | ||
| 279 | highlight_border: None, | ||
| 280 | // `$border_color`. | ||
| 281 | rule: ink(0.15), | ||
| 282 | font_size: points * 4.0 / 3.0, | ||
| 283 | radius: 15.0, | ||
| 284 | pad: 6.0, | ||
| 285 | row: 32.0, | ||
| 286 | row_radius: 9.0, | ||
| 287 | row_pad: 12.0, | ||
| 288 | rule_band: 13.0, | ||
| 289 | rule_inset: 0.0, | ||
| 290 | motion: ui::PopupMotion::Cut, | ||
| 291 | } | ||
| 292 | } | ||
| 293 | |||
| 294 | /// Breeze's `QMenu` in `appearance`, coloured by kdeglobals `files` where the scheme suits | ||
| 295 | /// it, and faded by KWin's Fading Popups unless kwinrc `kwin` turns them off. | ||
| 296 | fn breeze_menu(appearance: Theme, files: &[String], kwin: &[String]) -> ui::Menu { | ||
| 297 | let entries = kconfig_entries(files); | ||
| 298 | let color = |group, key| kde_color(entries.get(&(group, key))?); | ||
| 299 | // Breeze Light's or Breeze Dark's window, its text, a view and the focus decoration. | ||
| 300 | let stock = if appearance == Theme::Dark { | ||
| 301 | [[32, 35, 38], [252; 3], [20, 22, 24], [61, 174, 233]] | ||
| 302 | } else { | ||
| 303 | [[239, 240, 241], [35, 38, 41], [255; 3], [61, 174, 233]] | ||
| 304 | }; | ||
| 305 | let scheme = [ | ||
| 306 | ("Colors:Window", "BackgroundNormal"), | ||
| 307 | ("Colors:Window", "ForegroundNormal"), | ||
| 308 | ("Colors:View", "BackgroundNormal"), | ||
| 309 | ("Colors:View", "DecorationFocus"), | ||
| 310 | ]; | ||
| 311 | let mut colors: [[u8; 3]; 4] = std::array::from_fn(|index| { | ||
| 312 | let (group, key) = scheme[index]; | ||
| 313 | color(group, key).unwrap_or(stock[index]) | ||
| 314 | }); | ||
| 315 | let window = linear(colors[0]); | ||
| 316 | let dark = 0.2126 * window[0] + 0.7152 * window[1] + 0.0722 * window[2] < 0.18; | ||
| 317 | if dark != (appearance == Theme::Dark) { | ||
| 318 | colors = stock; | ||
| 319 | } | ||
| 320 | let [window, text, view, focus] = colors; | ||
| 321 | // As `Helper::frameBackgroundColor`, `frameOutlineColor` and `focusOutlineColor` mix them. | ||
| 322 | let fill = over(view, 0.3, window); | ||
| 323 | let outline = linear(over(text, 0.2, window)); | ||
| 324 | let kwin = kconfig_entries(kwin); | ||
| 325 | let factor = entries | ||
| 326 | .get(&("KDE", "AnimationDurationFactor")) | ||
| 327 | .and_then(|factor| factor.parse().ok()) | ||
| 328 | .unwrap_or(1.0_f32); | ||
| 329 | let fades = kwin | ||
| 330 | .get(&("Plugins", "fadingpopupsEnabled")) | ||
| 331 | .is_none_or(|enabled| *enabled != "false"); | ||
| 332 | let points = entries | ||
| 333 | .get(&("General", "font")) | ||
| 334 | .and_then(|font| font.split(',').nth(1)?.parse().ok()) | ||
| 335 | .unwrap_or(10.0_f32); | ||
| 336 | ui::Menu { | ||
| 337 | fill: linear(fill), | ||
| 338 | border: outline, | ||
| 339 | // `ShadowHelper`'s default large shadow, which KWin draws around the menu. | ||
| 340 | shadows: vec![ | ||
| 341 | css_shadow(0.22, 20.0, 5.0, 0.0), | ||
| 342 | css_shadow(0.12, 10.0, 2.0, 0.0), | ||
| 343 | ], | ||
| 344 | text: linear(text), | ||
| 345 | // Accelerators draw at 70% opacity; disabled text approximates the scheme's fade. | ||
| 346 | dim: linear(over(text, 0.7, fill)), | ||
| 347 | disabled: linear(over(text, 0.35, fill)), | ||
| 348 | highlight: linear(over(focus, 0.3, fill)), | ||
| 349 | highlight_border: Some(linear(over(text, 0.15, focus))), | ||
| 350 | rule: outline, | ||
| 351 | font_size: points * 4.0 / 3.0, | ||
| 352 | radius: 5.0, | ||
| 353 | pad: 4.0, | ||
| 354 | row: 26.0, | ||
| 355 | row_radius: 5.0, | ||
| 356 | // `MenuItem_MarginWidth` and `MenuItem_ExtraLeftMargin`. | ||
| 357 | row_pad: 9.0, | ||
| 358 | rule_band: 7.0, | ||
| 359 | rule_inset: 5.0, | ||
| 360 | motion: if fades && factor > 0.0 { | ||
| 361 | ui::PopupMotion::Fade([0.15 * factor, 0.6 * factor]) | ||
| 362 | } else { | ||
| 363 | ui::PopupMotion::Cut | ||
| 364 | }, | ||
| 365 | } | ||
| 366 | } | ||
| 367 | |||
| 368 | /// `top` over `under` at `alpha`, blended in sRGB as GTK and Qt paint. | ||
| 369 | fn over(top: [u8; 3], alpha: f32, under: [u8; 3]) -> [u8; 3] { | ||
| 370 | std::array::from_fn(|channel| { | ||
| 371 | let [top, under] = [top[channel], under[channel]].map(f32::from); | ||
| 372 | (under + (top - under) * alpha).round() as u8 | ||
| 373 | }) | ||
| 374 | } | ||
| 375 | |||
| 376 | fn linear([red, green, blue]: [u8; 3]) -> [f32; 4] { | ||
| 377 | draw::srgb(red, green, blue) | ||
| 378 | } | ||
| 379 | |||
| 380 | /// A black `box-shadow` at `alpha`, as dark over white as GTK and Qt blend it in sRGB. | ||
| 381 | fn css_shadow(alpha: f32, blur: f32, drop: f32, spread: f32) -> ui::Shadow { | ||
| 382 | let [left, ..] = linear(over([0; 3], alpha, [0xff; 3])); | ||
| 383 | ui::Shadow { | ||
| 384 | color: [0.0, 0.0, 0.0, 1.0 - left], | ||
| 385 | blur, | ||
| 386 | drop, | ||
| 387 | spread, | ||
| 388 | } | ||
| 389 | } | ||
| 390 | |||
| 217 | /// KConfig file `name` from the system's configuration directories up to the user's, in | 391 | /// KConfig file `name` from the system's configuration directories up to the user's, in |
| 218 | /// rising precedence. | 392 | /// rising precedence. |
| 219 | fn kconfig(name: &str) -> Vec<String> { | 393 | fn kconfig(name: &str) -> Vec<String> { |
| ... | @@ -373,8 +547,10 @@ pub fn update_menu(_: impl FnOnce() -> Vec<crate::commands::Status>) {} | ... | @@ -373,8 +547,10 @@ pub fn update_menu(_: impl FnOnce() -> Vec<crate::commands::Status>) {} |
| 373 | 547 | ||
| 374 | pub fn clear_marked_text(_: &Window) {} | 548 | pub fn clear_marked_text(_: &Window) {} |
| 375 | 549 | ||
| 550 | #[cfg(feature = "wgpu")] | ||
| 376 | pub fn configure_presentation(_: &wgpu::Surface<'_>) {} | 551 | pub fn configure_presentation(_: &wgpu::Surface<'_>) {} |
| 377 | 552 | ||
| 553 | #[cfg(feature = "wgpu")] | ||
| 378 | pub fn commit_presentation(_: &Window) {} | 554 | pub fn commit_presentation(_: &Window) {} |
| 379 | 555 | ||
| 380 | /// GTK's default double-click time. | 556 | /// GTK's default double-click time. |
| ... | @@ -426,6 +602,16 @@ fn watch_settings(proxy: EventLoopProxy<crate::UserEvent>) { | ... | @@ -426,6 +602,16 @@ fn watch_settings(proxy: EventLoopProxy<crate::UserEvent>) { |
| 426 | } | 602 | } |
| 427 | 603 | ||
| 428 | fn portal_color_scheme() -> Option<bool> { | 604 | fn portal_color_scheme() -> Option<bool> { |
| 605 | let scheme = portal_setting("org.freedesktop.appearance", "color-scheme")?; | ||
| 606 | match u32::try_from(scheme).ok()? { | ||
| 607 | 1 => Some(true), | ||
| 608 | 2 => Some(false), | ||
| 609 | _ => None, | ||
| 610 | } | ||
| 611 | } | ||
| 612 | |||
| 613 | /// Setting `key` in `namespace` from the settings portal. | ||
| 614 | fn portal_setting(namespace: &str, key: &str) -> Option<zbus::zvariant::OwnedValue> { | ||
| 429 | use zbus::zvariant::{OwnedValue, Value}; | 615 | use zbus::zvariant::{OwnedValue, Value}; |
| 430 | // A missing portal must not hold up the window for D-Bus's 25 s default. | 616 | // A missing portal must not hold up the window for D-Bus's 25 s default. |
| 431 | let connection = zbus::blocking::connection::Builder::session() | 617 | let connection = zbus::blocking::connection::Builder::session() |
| ... | @@ -440,7 +626,7 @@ fn portal_color_scheme() -> Option<bool> { | ... | @@ -440,7 +626,7 @@ fn portal_color_scheme() -> Option<bool> { |
| 440 | "org.freedesktop.portal.Settings", | 626 | "org.freedesktop.portal.Settings", |
| 441 | ) | 627 | ) |
| 442 | .ok()?; | 628 | .ok()?; |
| 443 | let key = ("org.freedesktop.appearance", "color-scheme"); | 629 | let key = (namespace, key); |
| 444 | // Portals before ReadOne answer Read, with the value in a second variant. | 630 | // Portals before ReadOne answer Read, with the value in a second variant. |
| 445 | let reply: OwnedValue = proxy | 631 | let reply: OwnedValue = proxy |
| 446 | .call("ReadOne", &key) | 632 | .call("ReadOne", &key) |
| ... | @@ -450,11 +636,7 @@ fn portal_color_scheme() -> Option<bool> { | ... | @@ -450,11 +636,7 @@ fn portal_color_scheme() -> Option<bool> { |
| 450 | while let Value::Value(inner) = value { | 636 | while let Value::Value(inner) = value { |
| 451 | value = *inner; | 637 | value = *inner; |
| 452 | } | 638 | } |
| 453 | match u32::try_from(value).ok()? { | 639 | value.try_into_owned().ok() |
| 454 | 1 => Some(true), | ||
| 455 | 2 => Some(false), | ||
| 456 | _ => None, | ||
| 457 | } | ||
| 458 | } | 640 | } |
| 459 | 641 | ||
| 460 | pub fn cache_dir() -> Option<PathBuf> { | 642 | pub fn cache_dir() -> Option<PathBuf> { |
| ... | @@ -793,6 +975,41 @@ fn dialog<const Z: usize, const K: usize>( | ... | @@ -793,6 +975,41 @@ fn dialog<const Z: usize, const K: usize>( |
| 793 | .then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())) | 975 | .then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())) |
| 794 | } | 976 | } |
| 795 | 977 | ||
| 978 | /// Asks for a picture to insert. | ||
| 979 | pub fn pick_picture() -> Option<PathBuf> { | ||
| 980 | let patterns = crate::PICTURE_TYPES | ||
| 981 | .map(|kind| format!("*.{kind}")) | ||
| 982 | .join(" "); | ||
| 983 | let asked = dialog( | ||
| 984 | [ | ||
| 985 | "--file-selection", | ||
| 986 | "--title=Insert Picture", | ||
| 987 | &format!("--file-filter=Pictures | {patterns}"), | ||
| 988 | ], | ||
| 989 | [ | ||
| 990 | "--getopenfilename", | ||
| 991 | ".", | ||
| 992 | &patterns, | ||
| 993 | "--title", | ||
| 994 | "Insert Picture", | ||
| 995 | ], | ||
| 996 | ); | ||
| 997 | asked | ||
| 998 | .unwrap_or_else(|error| { | ||
| 999 | eprintln!("{error}"); | ||
| 1000 | None | ||
| 1001 | }) | ||
| 1002 | .filter(|path| !path.is_empty()) | ||
| 1003 | .map(PathBuf::from) | ||
| 1004 | } | ||
| 1005 | |||
| 1006 | /// Opens GNOME's character map, whose picks the user copies in. | ||
| 1007 | pub fn character_palette() { | ||
| 1008 | if let Err(error) = Command::new("gnome-characters").spawn() { | ||
| 1009 | eprintln!("Cannot open the character map: {error}"); | ||
| 1010 | } | ||
| 1011 | } | ||
| 1012 | |||
| 796 | /// Opens `target`, a folder or a link's URL, with the desktop's handler. | 1013 | /// Opens `target`, a folder or a link's URL, with the desktop's handler. |
| 797 | pub fn reveal(target: impl AsRef<std::ffi::OsStr>) { | 1014 | pub fn reveal(target: impl AsRef<std::ffi::OsStr>) { |
| 798 | let target = target.as_ref(); | 1015 | let target = target.as_ref(); |
| ... | @@ -963,6 +1180,51 @@ mod tests { | ... | @@ -963,6 +1180,51 @@ mod tests { |
| 963 | ); | 1180 | ); |
| 964 | } | 1181 | } |
| 965 | 1182 | ||
| 1183 | #[test] | ||
| 1184 | fn breeze_menus_take_the_scheme_s_colours_and_kwin_s_fade() { | ||
| 1185 | let srgb = |[red, green, blue]: [u8; 3]| draw::srgb(red, green, blue); | ||
| 1186 | let menu = breeze_menu(Theme::Light, &[], &[]); | ||
| 1187 | // As measured from Dolphin's context menu under Breeze Light. | ||
| 1188 | assert_eq!(menu.fill, srgb([0xf4, 0xf5, 0xf5])); | ||
| 1189 | assert_eq!(menu.border, srgb([0xc6, 0xc8, 0xc9])); | ||
| 1190 | assert_eq!(menu.highlight, srgb([0xbd, 0xe0, 0xf1])); | ||
| 1191 | assert_eq!(menu.highlight_border, Some(srgb([0x39, 0x9a, 0xcc]))); | ||
| 1192 | assert_eq!(menu.font_size, 10.0 * 4.0 / 3.0); | ||
| 1193 | assert_eq!(menu.motion, ui::PopupMotion::Fade([0.15, 0.6])); | ||
| 1194 | |||
| 1195 | let settings = "[General]\nfont=Noto Sans,12,-1,5,400,0,0,0,0,0\n\ | ||
| 1196 | [KDE]\nAnimationDurationFactor=0.5\n\ | ||
| 1197 | [Colors:Window]\nBackgroundNormal=32,35,38\nForegroundNormal=252,252,252\n"; | ||
| 1198 | let menu = breeze_menu(Theme::Dark, &[settings.into()], &[]); | ||
| 1199 | assert_eq!(menu.text, srgb([252; 3])); | ||
| 1200 | assert_eq!(menu.font_size, 16.0); | ||
| 1201 | assert_eq!(menu.motion, ui::PopupMotion::Fade([0.075, 0.3])); | ||
| 1202 | assert_eq!( | ||
| 1203 | breeze_menu(Theme::Light, &[settings.into()], &[]).fill, | ||
| 1204 | srgb([0xf4, 0xf5, 0xf5]), | ||
| 1205 | "a dark scheme under a light appearance takes Breeze Light's" | ||
| 1206 | ); | ||
| 1207 | |||
| 1208 | let instant = "[KDE]\nAnimationDurationFactor=0\n"; | ||
| 1209 | let unfaded = "[Plugins]\nfadingpopupsEnabled=false\n"; | ||
| 1210 | for (kdeglobals, kwinrc) in [(instant, ""), ("", unfaded)] { | ||
| 1211 | let motion = breeze_menu(Theme::Light, &[kdeglobals.into()], &[kwinrc.into()]).motion; | ||
| 1212 | assert_eq!(motion, ui::PopupMotion::Cut); | ||
| 1213 | } | ||
| 1214 | } | ||
| 1215 | |||
| 1216 | #[test] | ||
| 1217 | fn adwaita_menus_mix_the_popover_s_ink_as_its_stylesheet_does() { | ||
| 1218 | let srgb = |[red, green, blue]: [u8; 3]| draw::srgb(red, green, blue); | ||
| 1219 | let light = adwaita_menu(Theme::Light, 11.0); | ||
| 1220 | assert_eq!(light.text, srgb([51, 51, 56])); | ||
| 1221 | assert_eq!(light.highlight, srgb([235; 3])); | ||
| 1222 | assert_eq!(light.border, srgb([219; 3])); | ||
| 1223 | let dark = adwaita_menu(Theme::Dark, 11.0); | ||
| 1224 | assert_eq!(dark.highlight, srgb([74, 74, 78])); | ||
| 1225 | assert_eq!(dark.motion, ui::PopupMotion::Cut); | ||
| 1226 | } | ||
| 1227 | |||
| 966 | #[test] | 1228 | #[test] |
| 967 | fn kconfig_colours_read_as_channels_or_hex() { | 1229 | fn kconfig_colours_read_as_channels_or_hex() { |
| 968 | assert_eq!(kde_color("222,224,226"), Some([222, 224, 226])); | 1230 | assert_eq!(kde_color("222,224,226"), Some([222, 224, 226])); |
crates/snowbound/src/macos.rs+60-4| ... | @@ -116,9 +116,15 @@ impl Clipboard { | ... | @@ -116,9 +116,15 @@ impl Clipboard { |
| 116 | /// AppKit draws the traffic lights. | 116 | /// AppKit draws the traffic lights. |
| 117 | pub fn window_controls(_: &mut ui::Ui, _: &Window) {} | 117 | pub fn window_controls(_: &mut ui::Ui, _: &Window) {} |
| 118 | 118 | ||
| 119 | /// The app draws the title bar around the traffic lights. | 119 | /// Whether AppKit draws the title bar: before 10.10 content can't extend under it, and |
| 120 | pub fn system_titlebar(_: &Window) -> bool { | 120 | /// otherwise the app draws it around the traffic lights. |
| 121 | false | 121 | pub fn system_titlebar(window: &Window) -> bool { |
| 122 | let window = ns_window(window); | ||
| 123 | unsafe { | ||
| 124 | let frame: NSRect = msg_send![&window, frame]; | ||
| 125 | let content: NSRect = msg_send![&window, contentRectForFrameRect: frame]; | ||
| 126 | content.size.height < frame.size.height | ||
| 127 | } | ||
| 122 | } | 128 | } |
| 123 | 129 | ||
| 124 | unsafe extern "C" fn no_document_drag( | 130 | unsafe extern "C" fn no_document_drag( |
| ... | @@ -163,6 +169,11 @@ pub fn represent(window: &Window, file: Option<&std::path::Path>) { | ... | @@ -163,6 +169,11 @@ pub fn represent(window: &Window, file: Option<&std::path::Path>) { |
| 163 | } | 169 | } |
| 164 | } | 170 | } |
| 165 | 171 | ||
| 172 | /// None: the kit's own menus stand in for AppKit's. | ||
| 173 | pub fn menu(_: winit::window::Theme) -> Option<ui::Menu> { | ||
| 174 | None | ||
| 175 | } | ||
| 176 | |||
| 166 | /// Nothing the backdrop leaves to colour. | 177 | /// Nothing the backdrop leaves to colour. |
| 167 | pub fn titlebar(_: winit::window::Theme) -> Option<[[f32; 4]; 2]> { | 178 | pub fn titlebar(_: winit::window::Theme) -> Option<[[f32; 4]; 2]> { |
| 168 | None | 179 | None |
| ... | @@ -178,6 +189,10 @@ unsafe extern "C" fn hit_nothing(_: &AnyObject, _: Sel, _: NSPoint) -> *mut AnyO | ... | @@ -178,6 +189,10 @@ unsafe extern "C" fn hit_nothing(_: &AnyObject, _: Sel, _: NSPoint) -> *mut AnyO |
| 178 | /// through it to the view it lies in. | 189 | /// through it to the view it lies in. |
| 179 | pub fn install_backdrop(window: &Window) -> bool { | 190 | pub fn install_backdrop(window: &Window) -> bool { |
| 180 | MainThreadMarker::new().expect("Views belong to the main thread"); | 191 | MainThreadMarker::new().expect("Views belong to the main thread"); |
| 192 | // Before 10.10 AppKit has no materials. | ||
| 193 | let Some(effect) = AnyClass::get("NSVisualEffectView") else { | ||
| 194 | return false; | ||
| 195 | }; | ||
| 181 | let RawWindowHandle::AppKit(handle) = | 196 | let RawWindowHandle::AppKit(handle) = |
| 182 | window.window_handle().expect("Live AppKit window").as_raw() | 197 | window.window_handle().expect("Live AppKit window").as_raw() |
| 183 | else { | 198 | else { |
| ... | @@ -186,7 +201,6 @@ pub fn install_backdrop(window: &Window) -> bool { | ... | @@ -186,7 +201,6 @@ pub fn install_backdrop(window: &Window) -> bool { |
| 186 | unsafe { | 201 | unsafe { |
| 187 | let view = &*handle.ns_view.as_ptr().cast::<AnyObject>(); | 202 | let view = &*handle.ns_view.as_ptr().cast::<AnyObject>(); |
| 188 | let class = BACKDROP_CLASS.get_or_init(|| { | 203 | let class = BACKDROP_CLASS.get_or_init(|| { |
| 189 | let effect = AnyClass::get("NSVisualEffectView").expect("AppKit is linked"); | ||
| 190 | let mut class = | 204 | let mut class = |
| 191 | ClassBuilder::new("SnowboundBackdrop", effect).expect("Unique backdrop class"); | 205 | ClassBuilder::new("SnowboundBackdrop", effect).expect("Unique backdrop class"); |
| 192 | class.add_method( | 206 | class.add_method( |
| ... | @@ -351,6 +365,46 @@ pub fn pick_new(title: &str, name: &str) -> Option<std::path::PathBuf> { | ... | @@ -351,6 +365,46 @@ pub fn pick_new(title: &str, name: &str) -> Option<std::path::PathBuf> { |
| 351 | } | 365 | } |
| 352 | } | 366 | } |
| 353 | 367 | ||
| 368 | /// Asks for a picture to insert, with the system's open panel. | ||
| 369 | pub fn pick_picture() -> Option<std::path::PathBuf> { | ||
| 370 | let mtm = MainThreadMarker::new().expect("Panels belong to the main thread"); | ||
| 371 | unsafe { | ||
| 372 | let panel = objc2_app_kit::NSOpenPanel::openPanel(mtm); | ||
| 373 | let types = crate::PICTURE_TYPES.map(NSString::from_str); | ||
| 374 | #[allow(deprecated, reason = "Allowed content types need macOS 11")] | ||
| 375 | panel.setAllowedFileTypes(Some(&objc2_foundation::NSArray::from_vec(types.into()))); | ||
| 376 | panel.setPrompt(Some(&NSString::from_str("Insert"))); | ||
| 377 | if panel.runModal() != objc2_app_kit::NSModalResponseOK { | ||
| 378 | return None; | ||
| 379 | } | ||
| 380 | let path = panel.URLs().firstObject()?.path()?; | ||
| 381 | Some(path.to_string().into()) | ||
| 382 | } | ||
| 383 | } | ||
| 384 | |||
| 385 | /// Lets the user drag out part of the screen, then sends it as a PNG. | ||
| 386 | pub fn clip_screen(proxy: EventLoopProxy<crate::UserEvent>) { | ||
| 387 | std::thread::spawn(move || { | ||
| 388 | let path = std::env::temp_dir().join(format!("snowbound-clip-{}.png", std::process::id())); | ||
| 389 | let taken = std::process::Command::new("/usr/sbin/screencapture") | ||
| 390 | .arg("-i") | ||
| 391 | .arg(&path) | ||
| 392 | .status() | ||
| 393 | .is_ok_and(|status| status.success()); | ||
| 394 | // Escape leaves no file. | ||
| 395 | if taken && let Ok(bytes) = std::fs::read(&path) { | ||
| 396 | let _ = std::fs::remove_file(&path); | ||
| 397 | let _ = proxy.send_event(crate::UserEvent::Picture(bytes)); | ||
| 398 | } | ||
| 399 | }); | ||
| 400 | } | ||
| 401 | |||
| 402 | /// Shows the system's character palette, whose picks arrive as inserted text. | ||
| 403 | pub fn character_palette() { | ||
| 404 | let mtm = MainThreadMarker::new().expect("AppKit belongs to the main thread"); | ||
| 405 | NSApplication::sharedApplication(mtm).orderFrontCharacterPalette(None); | ||
| 406 | } | ||
| 407 | |||
| 354 | /// Tells the user something they asked for could not be done: `message`, then what to do. | 408 | /// Tells the user something they asked for could not be done: `message`, then what to do. |
| 355 | pub fn alert(message: &str, detail: &str) { | 409 | pub fn alert(message: &str, detail: &str) { |
| 356 | let mtm = MainThreadMarker::new().expect("Window events run on the main thread"); | 410 | let mtm = MainThreadMarker::new().expect("Window events run on the main thread"); |
| ... | @@ -598,6 +652,7 @@ mod date_tests { | ... | @@ -598,6 +652,7 @@ mod date_tests { |
| 598 | } | 652 | } |
| 599 | } | 653 | } |
| 600 | 654 | ||
| 655 | #[cfg(feature = "wgpu")] | ||
| 601 | pub fn configure_presentation(surface: &wgpu::Surface<'_>) { | 656 | pub fn configure_presentation(surface: &wgpu::Surface<'_>) { |
| 602 | MainThreadMarker::new().expect("Layer presentation belongs to the main thread"); | 657 | MainThreadMarker::new().expect("Layer presentation belongs to the main thread"); |
| 603 | // Retain wgpu's surface ownership while synchronizing presentation with AppKit resize transactions. | 658 | // Retain wgpu's surface ownership while synchronizing presentation with AppKit resize transactions. |
| ... | @@ -612,6 +667,7 @@ pub fn configure_presentation(surface: &wgpu::Surface<'_>) { | ... | @@ -612,6 +667,7 @@ pub fn configure_presentation(surface: &wgpu::Surface<'_>) { |
| 612 | /// Commits a frame presented with the transaction: winit redraws after Core Animation's | 667 | /// Commits a frame presented with the transaction: winit redraws after Core Animation's |
| 613 | /// commit observer, so otherwise the frame waits for the next event. A live resize leaves | 668 | /// commit observer, so otherwise the frame waits for the next event. A live resize leaves |
| 614 | /// the commit to AppKit, which pairs the frame with the window's new size. | 669 | /// the commit to AppKit, which pairs the frame with the window's new size. |
| 670 | #[cfg(feature = "wgpu")] | ||
| 615 | pub fn commit_presentation(window: &Window) { | 671 | pub fn commit_presentation(window: &Window) { |
| 616 | let resizing: bool = unsafe { msg_send![&ns_window(window), inLiveResize] }; | 672 | let resizing: bool = unsafe { msg_send![&ns_window(window), inLiveResize] }; |
| 617 | if !resizing { | 673 | if !resizing { |
crates/snowbound/src/main.rs+429-332| ... | @@ -20,6 +20,9 @@ mod screenshot; | ... | @@ -20,6 +20,9 @@ mod screenshot; |
| 20 | mod search; | 20 | mod search; |
| 21 | mod settings; | 21 | mod settings; |
| 22 | mod sidebar; | 22 | mod sidebar; |
| 23 | #[cfg_attr(not(feature = "wgpu"), path = "surface_gl.rs")] | ||
| 24 | mod surface; | ||
| 25 | mod sync; | ||
| 23 | mod templates; | 26 | mod templates; |
| 24 | 27 | ||
| 25 | use canvas::gpu::page::PageScene; | 28 | use canvas::gpu::page::PageScene; |
| ... | @@ -33,6 +36,7 @@ use canvas::{ | ... | @@ -33,6 +36,7 @@ use canvas::{ |
| 33 | use canvas::{ | 36 | use canvas::{ |
| 34 | gpu::{colorref, tag_sources}, | 37 | gpu::{colorref, tag_sources}, |
| 35 | outline::TagIcon, | 38 | outline::TagIcon, |
| 39 | template::PAGE_COLORS, | ||
| 36 | }; | 40 | }; |
| 37 | use draw::Renderer; | 41 | use draw::Renderer; |
| 38 | use library::Library; | 42 | use library::Library; |
| ... | @@ -63,6 +67,8 @@ const TAB_ROW: f32 = 28.0; | ... | @@ -63,6 +67,8 @@ const TAB_ROW: f32 = 28.0; |
| 63 | /// Width of the section colour around the page. | 67 | /// Width of the section colour around the page. |
| 64 | const FRAME: f32 = 6.0; | 68 | const FRAME: f32 = 6.0; |
| 65 | const PAGE_LIST: f32 = 240.0; | 69 | const PAGE_LIST: f32 = 240.0; |
| 70 | /// Extensions of the pictures Insert, Picture offers: those the page both stores and draws. | ||
| 71 | const PICTURE_TYPES: [&str; 4] = ["png", "jpg", "jpeg", "gif"]; | ||
| 66 | /// Font sizes the size box offers, OneNote's list in points. | 72 | /// Font sizes the size box offers, OneNote's list in points. |
| 67 | /// Fonts the font box offers first, OneNote's default leading. | 73 | /// Fonts the font box offers first, OneNote's default leading. |
| 68 | const FONTS: [&str; 4] = ["Calibri", "Arial", "Times New Roman", "Courier New"]; | 74 | const FONTS: [&str; 4] = ["Calibri", "Arial", "Times New Roman", "Courier New"]; |
| ... | @@ -116,6 +122,9 @@ enum UserEvent { | ... | @@ -116,6 +122,9 @@ enum UserEvent { |
| 116 | /// Text AppKit inserts outside key events, such as the character palette's. | 122 | /// Text AppKit inserts outside key events, such as the character palette's. |
| 117 | #[cfg_attr(not(target_os = "macos"), allow(dead_code))] | 123 | #[cfg_attr(not(target_os = "macos"), allow(dead_code))] |
| 118 | InsertText(String), | 124 | InsertText(String), |
| 125 | /// A picture to insert at the caret, such as a screen clipping. | ||
| 126 | #[cfg_attr(not(target_os = "macos"), allow(dead_code))] | ||
| 127 | Picture(Vec<u8>), | ||
| 119 | Accessibility(accesskit_winit::Event), | 128 | Accessibility(accesskit_winit::Event), |
| 120 | /// The section's synchronization thread reported an event. | 129 | /// The section's synchronization thread reported an event. |
| 121 | Sync, | 130 | Sync, |
| ... | @@ -233,7 +242,8 @@ struct Session { | ... | @@ -233,7 +242,8 @@ struct Session { |
| 233 | /// Spaces, titles and outline levels in section order. | 242 | /// Spaces, titles and outline levels in section order. |
| 234 | pages: Vec<(ExGuid, String, u32)>, | 243 | pages: Vec<(ExGuid, String, u32)>, |
| 235 | space: ExGuid, | 244 | space: ExGuid, |
| 236 | status: &'static str, | 245 | /// The section's sync status as last read. |
| 246 | sync: notebook::session::SyncStatus, | ||
| 237 | /// Each page's conflict pages, the versions a merge could not take. | 247 | /// Each page's conflict pages, the versions a merge could not take. |
| 238 | conflicts: Vec<(ExGuid, Vec<onestore::ConflictPage>)>, | 248 | conflicts: Vec<(ExGuid, Vec<onestore::ConflictPage>)>, |
| 239 | /// The page whose conflict pages the list shows beneath it. | 249 | /// The page whose conflict pages the list shows beneath it. |
| ... | @@ -268,16 +278,6 @@ enum Bar { | ... | @@ -268,16 +278,6 @@ enum Bar { |
| 268 | } | 278 | } |
| 269 | 279 | ||
| 270 | impl Session { | 280 | impl Session { |
| 271 | /// Saving's state, and that the notebook opened through the system's mount of its | ||
| 272 | /// share where Snowbound's own SMB client could not sign in. | ||
| 273 | fn status(&self) -> String { | ||
| 274 | match (&self.library.notice, self.status) { | ||
| 275 | (None, status) => status.to_owned(), | ||
| 276 | (Some(_), "") => "Via mounted share".to_owned(), | ||
| 277 | (Some(_), status) => format!("{status} · Via mounted share"), | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | fn title(&self) -> &str { | 281 | fn title(&self) -> &str { |
| 282 | match self.conflict(self.space) { | 282 | match self.conflict(self.space) { |
| 283 | Some((_, version)) => &version.title, | 283 | Some((_, version)) => &version.title, |
| ... | @@ -491,9 +491,7 @@ struct State { | ... | @@ -491,9 +491,7 @@ struct State { |
| 491 | trail: navigation::Trail, | 491 | trail: navigation::Trail, |
| 492 | /// Asks for a frame when a worker thread finishes something the page shows. | 492 | /// Asks for a frame when a worker thread finishes something the page shows. |
| 493 | redraw: std::task::Waker, | 493 | redraw: std::task::Waker, |
| 494 | instance: wgpu::Instance, | 494 | surface: surface::Surface, |
| 495 | surface: wgpu::Surface<'static>, | ||
| 496 | config: wgpu::SurfaceConfiguration, | ||
| 497 | renderer: Renderer, | 495 | renderer: Renderer, |
| 498 | ui: Ui, | 496 | ui: Ui, |
| 499 | view: PageView, | 497 | view: PageView, |
| ... | @@ -520,8 +518,6 @@ struct State { | ... | @@ -520,8 +518,6 @@ struct State { |
| 520 | text_menu: Option<(canvas::interaction::Context, [f32; 2])>, | 518 | text_menu: Option<(canvas::interaction::Context, [f32; 2])>, |
| 521 | color_scheme: settings::ColorScheme, | 519 | color_scheme: settings::ColorScheme, |
| 522 | light_pages: bool, | 520 | light_pages: bool, |
| 523 | /// Carries frames to the window where the system's backdrop shows through the strip. | ||
| 524 | translucent: Option<draw::Translucent>, | ||
| 525 | /// The strip's fill with the window focused and not, continuing the system's title bar. | 521 | /// The strip's fill with the window focused and not, continuing the system's title bar. |
| 526 | titlebar: [[f32; 4]; 2], | 522 | titlebar: [[f32; 4]; 2], |
| 527 | /// A section or group being renamed in the sidebar. | 523 | /// A section or group being renamed in the sidebar. |
| ... | @@ -541,6 +537,10 @@ struct State { | ... | @@ -541,6 +537,10 @@ struct State { |
| 541 | search: search::Search, | 537 | search: search::Search, |
| 542 | /// Whether the page list is shown beside the page. | 538 | /// Whether the page list is shown beside the page. |
| 543 | pages_open: bool, | 539 | pages_open: bool, |
| 540 | /// Full Page View: the sidebar, section tabs and page list hidden around the page. | ||
| 541 | full_page: bool, | ||
| 542 | /// What Format Painter picked up, which the next selection made on the page takes. | ||
| 543 | painter: Option<onestore::document::Format>, | ||
| 544 | /// Installed font families, for the font box; listing them is slow. | 544 | /// Installed font families, for the font box; listing them is slow. |
| 545 | fonts: Vec<String>, | 545 | fonts: Vec<String>, |
| 546 | /// Fonts picked from the font box this session, latest first. | 546 | /// Fonts picked from the font box this session, latest first. |
| ... | @@ -631,32 +631,8 @@ impl State { | ... | @@ -631,32 +631,8 @@ impl State { |
| 631 | let access_adapter = | 631 | let access_adapter = |
| 632 | accesskit_winit::Adapter::with_event_loop_proxy(event_loop, &window, proxy.clone()); | 632 | accesskit_winit::Adapter::with_event_loop_proxy(event_loop, &window, proxy.clone()); |
| 633 | window.set_visible(visible); | 633 | window.set_visible(visible); |
| 634 | let instance = wgpu::Instance::new( | 634 | let (surface, renderer) = surface::Surface::new(window.clone(), backdrop).await?; |
| 635 | wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(window.clone())), | ||
| 636 | ); | ||
| 637 | let surface = instance.create_surface(window.clone())?; | ||
| 638 | platform::configure_presentation(&surface); | ||
| 639 | let adapter = instance | ||
| 640 | .request_adapter(&wgpu::RequestAdapterOptions { | ||
| 641 | compatible_surface: Some(&surface), | ||
| 642 | ..Default::default() | ||
| 643 | }) | ||
| 644 | .await?; | ||
| 645 | let (device, queue) = adapter.request_device(&Default::default()).await?; | ||
| 646 | let size = window.inner_size(); | 635 | let size = window.inner_size(); |
| 647 | let mut config = surface | ||
| 648 | .get_default_config(&adapter, size.width, size.height) | ||
| 649 | .ok_or("No supported canvas surface")?; | ||
| 650 | let translucent = backdrop.then(|| { | ||
| 651 | // What wgpu calls post-multiplied is Core Animation's non-opaque layer, which | ||
| 652 | // composites colour premultiplied in sRGB, as `Translucent` leaves it. | ||
| 653 | config.alpha_mode = wgpu::CompositeAlphaMode::PostMultiplied; | ||
| 654 | let window_format = config.format.remove_srgb_suffix(); | ||
| 655 | config.view_formats.push(window_format); | ||
| 656 | draw::Translucent::new(&device, config.format, window_format) | ||
| 657 | }); | ||
| 658 | surface.configure(&device, &config); | ||
| 659 | let renderer = Renderer::new(device, queue, config.format); | ||
| 660 | let mut engine = TextEngine::default(); | 636 | let mut engine = TextEngine::default(); |
| 661 | for path in substitutes { | 637 | for path in substitutes { |
| 662 | let target = engine | 638 | let target = engine |
| ... | @@ -754,7 +730,7 @@ impl State { | ... | @@ -754,7 +730,7 @@ impl State { |
| 754 | let dpr = window.scale_factor() as f32; | 730 | let dpr = window.scale_factor() as f32; |
| 755 | window.set_ime_allowed(true); | 731 | window.set_ime_allowed(true); |
| 756 | window.request_redraw(); | 732 | window.request_redraw(); |
| 757 | eprintln!("Canvas GPU: {:?}; scale factor {dpr}", adapter.get_info()); | 733 | eprintln!("Scale factor {dpr}"); |
| 758 | window.set_theme(stored.color_scheme.theme()); | 734 | window.set_theme(stored.color_scheme.theme()); |
| 759 | let appearance = stored | 735 | let appearance = stored |
| 760 | .color_scheme | 736 | .color_scheme |
| ... | @@ -791,9 +767,7 @@ impl State { | ... | @@ -791,9 +767,7 @@ impl State { |
| 791 | window, | 767 | window, |
| 792 | redraw, | 768 | redraw, |
| 793 | proxy, | 769 | proxy, |
| 794 | instance, | ||
| 795 | surface, | 770 | surface, |
| 796 | config, | ||
| 797 | renderer, | 771 | renderer, |
| 798 | ui, | 772 | ui, |
| 799 | view: PageView::new( | 773 | view: PageView::new( |
| ... | @@ -819,7 +793,6 @@ impl State { | ... | @@ -819,7 +793,6 @@ impl State { |
| 819 | options: None, | 793 | options: None, |
| 820 | color_scheme: stored.color_scheme, | 794 | color_scheme: stored.color_scheme, |
| 821 | light_pages: stored.light_pages, | 795 | light_pages: stored.light_pages, |
| 822 | translucent, | ||
| 823 | titlebar, | 796 | titlebar, |
| 824 | renaming: None, | 797 | renaming: None, |
| 825 | sectionless, | 798 | sectionless, |
| ... | @@ -828,6 +801,8 @@ impl State { | ... | @@ -828,6 +801,8 @@ impl State { |
| 828 | thumbnails: templates::Thumbnails::default(), | 801 | thumbnails: templates::Thumbnails::default(), |
| 829 | search, | 802 | search, |
| 830 | pages_open: true, | 803 | pages_open: true, |
| 804 | full_page: false, | ||
| 805 | painter: None, | ||
| 831 | fonts, | 806 | fonts, |
| 832 | recent_fonts: stored.recent_fonts, | 807 | recent_fonts: stored.recent_fonts, |
| 833 | toolbar: stored.toolbar, | 808 | toolbar: stored.toolbar, |
| ... | @@ -928,6 +903,7 @@ impl State { | ... | @@ -928,6 +903,7 @@ impl State { |
| 928 | let (section, open_tab, open_page) = self.build()?; | 903 | let (section, open_tab, open_page) = self.build()?; |
| 929 | self.options_dialog(); | 904 | self.options_dialog(); |
| 930 | self.link_dialog()?; | 905 | self.link_dialog()?; |
| 906 | self.sync_popup()?; | ||
| 931 | self.text_menu()?; | 907 | self.text_menu()?; |
| 932 | self.ui.end(); | 908 | self.ui.end(); |
| 933 | self.edges(section, open_tab, open_page); | 909 | self.edges(section, open_tab, open_page); |
| ... | @@ -946,7 +922,7 @@ impl State { | ... | @@ -946,7 +922,7 @@ impl State { |
| 946 | 922 | ||
| 947 | /// Takes `appearance`'s colours. | 923 | /// Takes `appearance`'s colours. |
| 948 | fn set_appearance(&mut self, appearance: winit::window::Theme) { | 924 | fn set_appearance(&mut self, appearance: winit::window::Theme) { |
| 949 | self.ui.theme = theme(appearance, self.light_pages, self.translucent.is_some()); | 925 | self.ui.theme = theme(appearance, self.light_pages, self.surface.translucent()); |
| 950 | self.titlebar = platform::titlebar(appearance).unwrap_or([self.ui.theme.strip; 2]); | 926 | self.titlebar = platform::titlebar(appearance).unwrap_or([self.ui.theme.strip; 2]); |
| 951 | } | 927 | } |
| 952 | 928 | ||
| ... | @@ -1027,10 +1003,15 @@ impl State { | ... | @@ -1027,10 +1003,15 @@ impl State { |
| 1027 | ..Spec::default() | 1003 | ..Spec::default() |
| 1028 | }, | 1004 | }, |
| 1029 | ); | 1005 | ); |
| 1006 | let tabs = self.ui.id("tabs"); | ||
| 1007 | let height = self | ||
| 1008 | .ui | ||
| 1009 | .animate(tabs, if self.full_page { 0.0 } else { TAB_ROW }); | ||
| 1030 | let tab_row = self.ui.open( | 1010 | let tab_row = self.ui.open( |
| 1031 | "tabs", | 1011 | "tabs", |
| 1032 | Spec { | 1012 | Spec { |
| 1033 | size: [fill(), px(TAB_ROW)], | 1013 | flags: Flags::CLIP, |
| 1014 | size: [fill(), px(height)], | ||
| 1034 | fill: Some(theme.strip), | 1015 | fill: Some(theme.strip), |
| 1035 | pad: [FRAME, 0.0], | 1016 | pad: [FRAME, 0.0], |
| 1036 | ..Spec::default() | 1017 | ..Spec::default() |
| ... | @@ -1216,6 +1197,12 @@ impl State { | ... | @@ -1216,6 +1197,12 @@ impl State { |
| 1216 | self.ui.close(); | 1197 | self.ui.close(); |
| 1217 | self.ui.close(); | 1198 | self.ui.close(); |
| 1218 | self.context_menu(); | 1199 | self.context_menu(); |
| 1200 | // No tab stands on the frame in Full Page View. | ||
| 1201 | let open_tab = if self.full_page { | ||
| 1202 | frame().child("no tab") | ||
| 1203 | } else { | ||
| 1204 | open_tab | ||
| 1205 | }; | ||
| 1219 | Ok((section, open_tab, open_page)) | 1206 | Ok((section, open_tab, open_page)) |
| 1220 | } | 1207 | } |
| 1221 | 1208 | ||
| ... | @@ -1332,7 +1319,7 @@ impl State { | ... | @@ -1332,7 +1319,7 @@ impl State { |
| 1332 | } | 1319 | } |
| 1333 | 1320 | ||
| 1334 | /// The window's title bar beside the traffic lights: the application's icon, the | 1321 | /// The window's title bar beside the traffic lights: the application's icon, the |
| 1335 | /// notebook's name and the saving status. | 1322 | /// notebook's name and the sync status. |
| 1336 | fn title_bar(&mut self, theme: &Theme) { | 1323 | fn title_bar(&mut self, theme: &Theme) { |
| 1337 | self.ui.open_as( | 1324 | self.ui.open_as( |
| 1338 | strip(), | 1325 | strip(), |
| ... | @@ -1374,23 +1361,14 @@ impl State { | ... | @@ -1374,23 +1361,14 @@ impl State { |
| 1374 | }, | 1361 | }, |
| 1375 | ); | 1362 | ); |
| 1376 | if let Some(session) = &self.session { | 1363 | if let Some(session) = &self.session { |
| 1377 | self.ui.leaf( | 1364 | sync::control(&mut self.ui, session, theme, TITLE); |
| 1378 | "status", | ||
| 1379 | Spec { | ||
| 1380 | size: [fit(), px(TITLE)], | ||
| 1381 | text: Some(&session.status()), | ||
| 1382 | color: Some(theme.text_dim), | ||
| 1383 | pad: [12.0, 0.0], | ||
| 1384 | ..Spec::default() | ||
| 1385 | }, | ||
| 1386 | ); | ||
| 1387 | } | 1365 | } |
| 1388 | platform::window_controls(&mut self.ui, &self.window); | 1366 | platform::window_controls(&mut self.ui, &self.window); |
| 1389 | self.ui.close(); | 1367 | self.ui.close(); |
| 1390 | } | 1368 | } |
| 1391 | 1369 | ||
| 1392 | /// Two rows of formatting, tag, insert and zoom buttons, grouped as OneNote's Home | 1370 | /// Two rows of navigation, clipboard, formatting, tag, insert and view buttons, grouped |
| 1393 | /// ribbon groups them. | 1371 | /// as OneNote's ribbon groups them. |
| 1394 | fn toolbar(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> { | 1372 | fn toolbar(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> { |
| 1395 | use canvas::editor::{ | 1373 | use canvas::editor::{ |
| 1396 | Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, NoteTag, Toggle, | 1374 | Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, NoteTag, Toggle, |
| ... | @@ -1398,13 +1376,28 @@ impl State { | ... | @@ -1398,13 +1376,28 @@ impl State { |
| 1398 | use commands::{Choice, Id as Cmd, shortcut}; | 1376 | use commands::{Choice, Id as Cmd, shortcut}; |
| 1399 | // A focused picture has no text to show a format for. | 1377 | // A focused picture has no text to show a format for. |
| 1400 | let state = self.format_state(); | 1378 | let state = self.format_state(); |
| 1379 | let statuses: Vec<_> = commands::COMMANDS | ||
| 1380 | .iter() | ||
| 1381 | .map(|command| { | ||
| 1382 | ( | ||
| 1383 | command.id, | ||
| 1384 | self.status(&Choice::Command(command.id), &state), | ||
| 1385 | ) | ||
| 1386 | }) | ||
| 1387 | .collect(); | ||
| 1388 | let status_of = |id| { | ||
| 1389 | statuses | ||
| 1390 | .iter() | ||
| 1391 | .find(|(listed, _)| *listed == id) | ||
| 1392 | .map_or_else(commands::Status::default, |(_, status)| *status) | ||
| 1393 | }; | ||
| 1401 | let fonts = &self.fonts; | 1394 | let fonts = &self.fonts; |
| 1402 | let recent_fonts = &self.recent_fonts; | 1395 | let recent_fonts = &self.recent_fonts; |
| 1403 | let pens = &self.toolbar; | 1396 | let pens = &self.toolbar; |
| 1404 | let engine = &self.view.engine; | 1397 | let engine = &self.view.engine; |
| 1405 | // Under the system's title bar the save status joins the toolbar. | 1398 | // Under the system's title bar the sync status joins the toolbar. |
| 1406 | let status = platform::system_titlebar(&self.window) | 1399 | let status = platform::system_titlebar(&self.window) |
| 1407 | .then(|| self.session.as_ref().map(Session::status)) | 1400 | .then_some(self.session.as_ref()) |
| 1408 | .flatten(); | 1401 | .flatten(); |
| 1409 | let ui = &mut self.ui; | 1402 | let ui = &mut self.ui; |
| 1410 | let text = theme.text; | 1403 | let text = theme.text; |
| ... | @@ -1420,16 +1413,80 @@ impl State { | ... | @@ -1420,16 +1413,80 @@ impl State { |
| 1420 | ..Spec::default() | 1413 | ..Spec::default() |
| 1421 | }, | 1414 | }, |
| 1422 | ); | 1415 | ); |
| 1423 | group(ui, "history", |ui| { | 1416 | group(ui, "navigate", |ui| { |
| 1424 | if ui::shell::tool_button(ui, "undo", art::UNDO, text, false).clicked { | 1417 | for (index, buttons) in [ |
| 1425 | choice = Some(Choice::Command(Cmd::Undo)); | 1418 | [ |
| 1426 | } | 1419 | ("back", art::BACK, Cmd::Back), |
| 1427 | if ui::shell::tool_button(ui, "redo", art::REDO, text, false).clicked { | 1420 | ("forward", art::FORWARD, Cmd::Forward), |
| 1428 | choice = Some(Choice::Command(Cmd::Redo)); | 1421 | ], |
| 1422 | [ | ||
| 1423 | ("undo", art::UNDO, Cmd::Undo), | ||
| 1424 | ("redo", art::REDO, Cmd::Redo), | ||
| 1425 | ], | ||
| 1426 | ] | ||
| 1427 | .into_iter() | ||
| 1428 | .enumerate() | ||
| 1429 | { | ||
| 1430 | row(ui, index, |ui| { | ||
| 1431 | for (part, icon, id) in buttons { | ||
| 1432 | choice = tool(ui, part, icon, text, id, status_of(id)).or(choice.take()); | ||
| 1433 | } | ||
| 1434 | }); | ||
| 1429 | } | 1435 | } |
| 1430 | }); | 1436 | }); |
| 1431 | divider(ui, "history", theme); | 1437 | divider(ui, "navigate", theme); |
| 1432 | let popup = |name: &str| Id::ROOT.child(("popup", name)); | 1438 | group(ui, "clipboard", |ui| { |
| 1439 | row(ui, 0, |ui| { | ||
| 1440 | let paste = ui.id("paste"); | ||
| 1441 | if !status_of(Cmd::Paste).enabled { | ||
| 1442 | ui::shell::unavailable(ui, "paste", art::PASTE, text, true); | ||
| 1443 | } else if ui::shell::split_button( | ||
| 1444 | ui, | ||
| 1445 | "paste", | ||
| 1446 | art::PASTE, | ||
| 1447 | None, | ||
| 1448 | false, | ||
| 1449 | toolbar_popup("paste"), | ||
| 1450 | ) | ||
| 1451 | .clicked | ||
| 1452 | { | ||
| 1453 | choice = Some(Choice::Command(Cmd::Paste)); | ||
| 1454 | } | ||
| 1455 | tip(ui, Cmd::Paste); | ||
| 1456 | // Pasting keeps only the text until #25. | ||
| 1457 | let options = [ | ||
| 1458 | ("Keep Source Formatting", false), | ||
| 1459 | ("Merge Formatting", false), | ||
| 1460 | ("Keep Text Only", true), | ||
| 1461 | ("Picture", false), | ||
| 1462 | ]; | ||
| 1463 | let mut items = vec![ui::popup::Item { | ||
| 1464 | text: "Paste Options", | ||
| 1465 | heading: true, | ||
| 1466 | ..Default::default() | ||
| 1467 | }]; | ||
| 1468 | items.extend(options.map(|(text, enabled)| ui::popup::Item { | ||
| 1469 | text, | ||
| 1470 | disabled: !enabled, | ||
| 1471 | ..Default::default() | ||
| 1472 | })); | ||
| 1473 | let anchor = ui::Anchor::Below(ui.rect(paste).unwrap_or_default()); | ||
| 1474 | if ui::popup::menu(ui, toolbar_popup("paste"), anchor, &items, None).is_some() { | ||
| 1475 | choice = Some(Choice::Command(Cmd::Paste)); | ||
| 1476 | } | ||
| 1477 | choice = tool(ui, "cut", art::CUT, text, Cmd::Cut, status_of(Cmd::Cut)) | ||
| 1478 | .or(choice.take()); | ||
| 1479 | }); | ||
| 1480 | row(ui, 1, |ui| { | ||
| 1481 | for (part, icon, id) in [ | ||
| 1482 | ("copy", art::COPY, Cmd::Copy), | ||
| 1483 | ("painter", art::FORMAT_PAINTER, Cmd::FormatPainter), | ||
| 1484 | ] { | ||
| 1485 | choice = tool(ui, part, icon, text, id, status_of(id)).or(choice.take()); | ||
| 1486 | } | ||
| 1487 | }); | ||
| 1488 | }); | ||
| 1489 | divider(ui, "clipboard", theme); | ||
| 1433 | let font = state.font.clone().unwrap_or_default(); | 1490 | let font = state.font.clone().unwrap_or_default(); |
| 1434 | let size = state | 1491 | let size = state |
| 1435 | .font_size | 1492 | .font_size |
| ... | @@ -1438,7 +1495,7 @@ impl State { | ... | @@ -1438,7 +1495,7 @@ impl State { |
| 1438 | row(ui, 0, |ui| { | 1495 | row(ui, 0, |ui| { |
| 1439 | let combo = ui.id("font"); | 1496 | let combo = ui.id("font"); |
| 1440 | if ui::shell::combo(ui, "font", &font, 120.0).pressed { | 1497 | if ui::shell::combo(ui, "font", &font, 120.0).pressed { |
| 1441 | ui.open_popup(popup("font")); | 1498 | ui.open_popup(toolbar_popup("font")); |
| 1442 | } | 1499 | } |
| 1443 | // Each family is named with the substitute it shows in, and picked by its | 1500 | // Each family is named with the substitute it shows in, and picked by its |
| 1444 | // own name; a family of None heads a group. Typing searches the full list. | 1501 | // own name; a family of None heads a group. Typing searches the full list. |
| ... | @@ -1449,7 +1506,7 @@ impl State { | ... | @@ -1449,7 +1506,7 @@ impl State { |
| 1449 | ) | 1506 | ) |
| 1450 | }; | 1507 | }; |
| 1451 | let mut choices: Vec<(String, Option<&str>, bool)> = Vec::new(); | 1508 | let mut choices: Vec<(String, Option<&str>, bool)> = Vec::new(); |
| 1452 | if ui::popup::query(ui, popup("font")).is_none_or(str::is_empty) { | 1509 | if ui::popup::query(ui, toolbar_popup("font")).is_none_or(str::is_empty) { |
| 1453 | choices.extend(FONTS.map(|name| (label(name), Some(name), false))); | 1510 | choices.extend(FONTS.map(|name| (label(name), Some(name), false))); |
| 1454 | if !recent_fonts.is_empty() { | 1511 | if !recent_fonts.is_empty() { |
| 1455 | choices.push(("Recent".to_owned(), None, true)); | 1512 | choices.push(("Recent".to_owned(), None, true)); |
| ... | @@ -1477,18 +1534,18 @@ impl State { | ... | @@ -1477,18 +1534,18 @@ impl State { |
| 1477 | .collect(); | 1534 | .collect(); |
| 1478 | let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default()); | 1535 | let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default()); |
| 1479 | if let Some(index) = | 1536 | if let Some(index) = |
| 1480 | ui::popup::menu(ui, popup("font"), anchor, &items, Some("Font")) | 1537 | ui::popup::menu(ui, toolbar_popup("font"), anchor, &items, Some("Font")) |
| 1481 | && let Some(name) = choices[index].1 | 1538 | && let Some(name) = choices[index].1 |
| 1482 | { | 1539 | { |
| 1483 | choice = Some(Choice::Font(name.to_owned())); | 1540 | choice = Some(Choice::Font(name.to_owned())); |
| 1484 | } | 1541 | } |
| 1485 | let combo = ui.id("size"); | 1542 | let combo = ui.id("size"); |
| 1486 | if ui::shell::combo(ui, "size", &size, 44.0).pressed { | 1543 | if ui::shell::combo(ui, "size", &size, 44.0).pressed { |
| 1487 | ui.open_popup(popup("size")); | 1544 | ui.open_popup(toolbar_popup("size")); |
| 1488 | } | 1545 | } |
| 1489 | // A size typed in the field joins the list, in half points as stored. | 1546 | // A size typed in the field joins the list, in half points as stored. |
| 1490 | let mut sizes = SIZES.to_vec(); | 1547 | let mut sizes = SIZES.to_vec(); |
| 1491 | if let Some(typed) = ui::popup::query(ui, popup("size")) | 1548 | if let Some(typed) = ui::popup::query(ui, toolbar_popup("size")) |
| 1492 | .and_then(|query| query.trim().parse::<f32>().ok()) | 1549 | .and_then(|query| query.trim().parse::<f32>().ok()) |
| 1493 | .map(|typed| (typed * 2.0).round() / 2.0) | 1550 | .map(|typed| (typed * 2.0).round() / 2.0) |
| 1494 | .filter(|typed| onestore::FONT_SIZES.contains(typed) && !sizes.contains(typed)) | 1551 | .filter(|typed| onestore::FONT_SIZES.contains(typed) && !sizes.contains(typed)) |
| ... | @@ -1506,7 +1563,8 @@ impl State { | ... | @@ -1506,7 +1563,8 @@ impl State { |
| 1506 | }) | 1563 | }) |
| 1507 | .collect(); | 1564 | .collect(); |
| 1508 | let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default()); | 1565 | let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default()); |
| 1509 | if let Some(index) = ui::popup::menu(ui, popup("size"), anchor, &items, Some(&size)) | 1566 | if let Some(index) = |
| 1567 | ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size)) | ||
| 1510 | { | 1568 | { |
| 1511 | choice = Some(Choice::Size(sizes[index])); | 1569 | choice = Some(Choice::Size(sizes[index])); |
| 1512 | } | 1570 | } |
| ... | @@ -1518,24 +1576,26 @@ impl State { | ... | @@ -1518,24 +1576,26 @@ impl State { |
| 1518 | art::BULLETS, | 1576 | art::BULLETS, |
| 1519 | None, | 1577 | None, |
| 1520 | state.bullets, | 1578 | state.bullets, |
| 1521 | popup("bullets"), | 1579 | toolbar_popup("bullets"), |
| 1522 | ) | 1580 | ) |
| 1523 | .clicked | 1581 | .clicked |
| 1524 | { | 1582 | { |
| 1525 | choice = Some(Choice::Command(Cmd::Bullets)); | 1583 | choice = Some(Choice::Command(Cmd::Bullets)); |
| 1526 | } | 1584 | } |
| 1585 | tip(ui, Cmd::Bullets); | ||
| 1527 | if ui::shell::split_button( | 1586 | if ui::shell::split_button( |
| 1528 | ui, | 1587 | ui, |
| 1529 | "numbering", | 1588 | "numbering", |
| 1530 | art::NUMBERING, | 1589 | art::NUMBERING, |
| 1531 | None, | 1590 | None, |
| 1532 | state.numbering, | 1591 | state.numbering, |
| 1533 | popup("numbering"), | 1592 | toolbar_popup("numbering"), |
| 1534 | ) | 1593 | ) |
| 1535 | .clicked | 1594 | .clicked |
| 1536 | { | 1595 | { |
| 1537 | choice = Some(Choice::Command(Cmd::Numbering)); | 1596 | choice = Some(Choice::Command(Cmd::Numbering)); |
| 1538 | } | 1597 | } |
| 1598 | tip(ui, Cmd::Numbering); | ||
| 1539 | let current = |bullet| match state.list { | 1599 | let current = |bullet| match state.list { |
| 1540 | Some(ListStyle::Bullet(place)) if bullet => Some(place), | 1600 | Some(ListStyle::Bullet(place)) if bullet => Some(place), |
| 1541 | Some(ListStyle::Number(place)) if !bullet => Some(place), | 1601 | Some(ListStyle::Number(place)) if !bullet => Some(place), |
| ... | @@ -1544,7 +1604,7 @@ impl State { | ... | @@ -1544,7 +1604,7 @@ impl State { |
| 1544 | let anchor = ui::Anchor::Below(ui.rect(bullets).unwrap_or_default()); | 1604 | let anchor = ui::Anchor::Below(ui.rect(bullets).unwrap_or_default()); |
| 1545 | if let Some(place) = list_gallery( | 1605 | if let Some(place) = list_gallery( |
| 1546 | ui, | 1606 | ui, |
| 1547 | popup("bullets"), | 1607 | toolbar_popup("bullets"), |
| 1548 | anchor, | 1608 | anchor, |
| 1549 | ["Recently Used Bullets", "Bullet Library"], | 1609 | ["Recently Used Bullets", "Bullet Library"], |
| 1550 | (&pens.bullets, BULLET_LIBRARY.len()), | 1610 | (&pens.bullets, BULLET_LIBRARY.len()), |
| ... | @@ -1570,7 +1630,7 @@ impl State { | ... | @@ -1570,7 +1630,7 @@ impl State { |
| 1570 | let anchor = ui::Anchor::Below(ui.rect(numbering).unwrap_or_default()); | 1630 | let anchor = ui::Anchor::Below(ui.rect(numbering).unwrap_or_default()); |
| 1571 | if let Some(place) = list_gallery( | 1631 | if let Some(place) = list_gallery( |
| 1572 | ui, | 1632 | ui, |
| 1573 | popup("numbering"), | 1633 | toolbar_popup("numbering"), |
| 1574 | anchor, | 1634 | anchor, |
| 1575 | ["Recently Used Number Formats", "Numbering Library"], | 1635 | ["Recently Used Number Formats", "Numbering Library"], |
| 1576 | (&pens.numbering, NUMBER_LIBRARY.len()), | 1636 | (&pens.numbering, NUMBER_LIBRARY.len()), |
| ... | @@ -1613,9 +1673,16 @@ impl State { | ... | @@ -1613,9 +1673,16 @@ impl State { |
| 1613 | ) { | 1673 | ) { |
| 1614 | choice = Some(Choice::List(place.map(ListStyle::Number))); | 1674 | choice = Some(Choice::List(place.map(ListStyle::Number))); |
| 1615 | } | 1675 | } |
| 1616 | if ui::shell::tool_button(ui, "clear", art::CLEAR_FORMATTING, text, false).clicked { | 1676 | let clear = Cmd::ClearFormatting; |
| 1617 | choice = Some(Choice::Command(Cmd::ClearFormatting)); | 1677 | choice = tool( |
| 1618 | } | 1678 | ui, |
| 1679 | "clear", | ||
| 1680 | art::CLEAR_FORMATTING, | ||
| 1681 | text, | ||
| 1682 | clear, | ||
| 1683 | status_of(clear), | ||
| 1684 | ) | ||
| 1685 | .or(choice.take()); | ||
| 1619 | }); | 1686 | }); |
| 1620 | row(ui, 1, |ui| { | 1687 | row(ui, 1, |ui| { |
| 1621 | for (part, icon, toggle) in [ | 1688 | for (part, icon, toggle) in [ |
| ... | @@ -1624,12 +1691,16 @@ impl State { | ... | @@ -1624,12 +1691,16 @@ impl State { |
| 1624 | ("underline", art::UNDERLINE, Toggle::Underline), | 1691 | ("underline", art::UNDERLINE, Toggle::Underline), |
| 1625 | ("strikethrough", art::STRIKETHROUGH, Toggle::Strikethrough), | 1692 | ("strikethrough", art::STRIKETHROUGH, Toggle::Strikethrough), |
| 1626 | ] { | 1693 | ] { |
| 1627 | if ui::shell::tool_button(ui, part, icon, text, on(toggle)).clicked { | 1694 | let id = Cmd::Toggle(toggle); |
| 1628 | choice = Some(Choice::Command(Cmd::Toggle(toggle))); | 1695 | choice = tool(ui, part, icon, text, id, status_of(id)).or(choice.take()); |
| 1629 | } | ||
| 1630 | } | 1696 | } |
| 1631 | let script = ui.id("script"); | 1697 | let script = ui.id("script"); |
| 1632 | let superscript = on(Toggle::Superscript); | 1698 | let superscript = on(Toggle::Superscript); |
| 1699 | let command = Cmd::Toggle(if superscript { | ||
| 1700 | Toggle::Superscript | ||
| 1701 | } else { | ||
| 1702 | Toggle::Subscript | ||
| 1703 | }); | ||
| 1633 | if ui::shell::split_button( | 1704 | if ui::shell::split_button( |
| 1634 | ui, | 1705 | ui, |
| 1635 | "script", | 1706 | "script", |
| ... | @@ -1640,16 +1711,13 @@ impl State { | ... | @@ -1640,16 +1711,13 @@ impl State { |
| 1640 | }, | 1711 | }, |
| 1641 | None, | 1712 | None, |
| 1642 | on(Toggle::Subscript) || superscript, | 1713 | on(Toggle::Subscript) || superscript, |
| 1643 | popup("script"), | 1714 | toolbar_popup("script"), |
| 1644 | ) | 1715 | ) |
| 1645 | .clicked | 1716 | .clicked |
| 1646 | { | 1717 | { |
| 1647 | choice = Some(Choice::Command(Cmd::Toggle(if superscript { | 1718 | choice = Some(Choice::Command(command)); |
| 1648 | Toggle::Superscript | ||
| 1649 | } else { | ||
| 1650 | Toggle::Subscript | ||
| 1651 | }))); | ||
| 1652 | } | 1719 | } |
| 1720 | tip(ui, command); | ||
| 1653 | let scripts = [ | 1721 | let scripts = [ |
| 1654 | (Toggle::Subscript, art::SUBSCRIPT), | 1722 | (Toggle::Subscript, art::SUBSCRIPT), |
| 1655 | (Toggle::Superscript, art::SUPERSCRIPT), | 1723 | (Toggle::Superscript, art::SUPERSCRIPT), |
| ... | @@ -1667,7 +1735,9 @@ impl State { | ... | @@ -1667,7 +1735,9 @@ impl State { |
| 1667 | }) | 1735 | }) |
| 1668 | .collect(); | 1736 | .collect(); |
| 1669 | let anchor = ui::Anchor::Below(ui.rect(script).unwrap_or_default()); | 1737 | let anchor = ui::Anchor::Below(ui.rect(script).unwrap_or_default()); |
| 1670 | if let Some(index) = ui::popup::menu(ui, popup("script"), anchor, &items, None) { | 1738 | if let Some(index) = |
| 1739 | ui::popup::menu(ui, toolbar_popup("script"), anchor, &items, None) | ||
| 1740 | { | ||
| 1671 | choice = Some(Choice::Command(Cmd::Toggle(scripts[index].0))); | 1741 | choice = Some(Choice::Command(Cmd::Toggle(scripts[index].0))); |
| 1672 | } | 1742 | } |
| 1673 | // Each button applies its menu's last pick: the highlighter shows it in its | 1743 | // Each button applies its menu's last pick: the highlighter shows it in its |
| ... | @@ -1691,19 +1761,22 @@ impl State { | ... | @@ -1691,19 +1761,22 @@ impl State { |
| 1691 | ), | 1761 | ), |
| 1692 | ] { | 1762 | ] { |
| 1693 | let split = ui.id(part); | 1763 | let split = ui.id(part); |
| 1694 | let button = ui::shell::split_button(ui, part, icon, bar, false, popup(part)); | 1764 | let button = |
| 1765 | ui::shell::split_button(ui, part, icon, bar, false, toolbar_popup(part)); | ||
| 1695 | let highlight = part == "highlight"; | 1766 | let highlight = part == "highlight"; |
| 1767 | let command = if highlight { | ||
| 1768 | Cmd::Highlight | ||
| 1769 | } else { | ||
| 1770 | Cmd::FontColor | ||
| 1771 | }; | ||
| 1696 | if button.clicked { | 1772 | if button.clicked { |
| 1697 | choice = Some(Choice::Command(if highlight { | 1773 | choice = Some(Choice::Command(command)); |
| 1698 | Cmd::Highlight | ||
| 1699 | } else { | ||
| 1700 | Cmd::FontColor | ||
| 1701 | })); | ||
| 1702 | } | 1774 | } |
| 1775 | tip(ui, command); | ||
| 1703 | let colors: Vec<_> = swatches.iter().map(|color| colorref(*color)).collect(); | 1776 | let colors: Vec<_> = swatches.iter().map(|color| colorref(*color)).collect(); |
| 1704 | let anchor = ui::Anchor::Below(ui.rect(split).unwrap_or_default()); | 1777 | let anchor = ui::Anchor::Below(ui.rect(split).unwrap_or_default()); |
| 1705 | if let Some(chosen) = | 1778 | if let Some(chosen) = |
| 1706 | ui::popup::colors(ui, popup(part), anchor, none, &colors, columns) | 1779 | ui::popup::colors(ui, toolbar_popup(part), anchor, none, &colors, columns) |
| 1707 | { | 1780 | { |
| 1708 | let color = chosen.and_then(|chosen| { | 1781 | let color = chosen.and_then(|chosen| { |
| 1709 | swatches | 1782 | swatches |
| ... | @@ -1719,11 +1792,11 @@ impl State { | ... | @@ -1719,11 +1792,11 @@ impl State { |
| 1719 | }); | 1792 | }); |
| 1720 | } | 1793 | } |
| 1721 | } | 1794 | } |
| 1722 | if ui::shell::tool_button(ui, "outdent", art::OUTDENT, text, false).clicked { | 1795 | for (part, icon, id) in [ |
| 1723 | choice = Some(Choice::Command(Cmd::Outdent)); | 1796 | ("outdent", art::OUTDENT, Cmd::Outdent), |
| 1724 | } | 1797 | ("indent", art::INDENT, Cmd::Indent), |
| 1725 | if ui::shell::tool_button(ui, "indent", art::INDENT, text, false).clicked { | 1798 | ] { |
| 1726 | choice = Some(Choice::Command(Cmd::Indent)); | 1799 | choice = tool(ui, part, icon, text, id, status_of(id)).or(choice.take()); |
| 1727 | } | 1800 | } |
| 1728 | let alignments = [ | 1801 | let alignments = [ |
| 1729 | (Alignment::Left, art::ALIGN_LEFT), | 1802 | (Alignment::Left, art::ALIGN_LEFT), |
| ... | @@ -1735,7 +1808,8 @@ impl State { | ... | @@ -1735,7 +1808,8 @@ impl State { |
| 1735 | .iter() | 1808 | .iter() |
| 1736 | .find(|(alignment, _)| Some(*alignment) == state.alignment) | 1809 | .find(|(alignment, _)| Some(*alignment) == state.alignment) |
| 1737 | .unwrap_or(&alignments[0]); | 1810 | .unwrap_or(&alignments[0]); |
| 1738 | let anchor = ui::shell::menu_button(ui, "align", current.1, popup("align")); | 1811 | let anchor = ui::shell::menu_button(ui, "align", current.1, toolbar_popup("align")); |
| 1812 | tip(ui, Cmd::Align(current.0)); | ||
| 1739 | // The current alignment starts highlighted, keeping its icon under the button's. | 1813 | // The current alignment starts highlighted, keeping its icon under the button's. |
| 1740 | let items: Vec<_> = alignments | 1814 | let items: Vec<_> = alignments |
| 1741 | .iter() | 1815 | .iter() |
| ... | @@ -1748,50 +1822,38 @@ impl State { | ... | @@ -1748,50 +1822,38 @@ impl State { |
| 1748 | ..Default::default() | 1822 | ..Default::default() |
| 1749 | }) | 1823 | }) |
| 1750 | .collect(); | 1824 | .collect(); |
| 1751 | if let Some(index) = ui::popup::menu(ui, popup("align"), anchor, &items, None) { | 1825 | if let Some(index) = |
| 1826 | ui::popup::menu(ui, toolbar_popup("align"), anchor, &items, None) | ||
| 1827 | { | ||
| 1752 | choice = Some(Choice::Command(Cmd::Align(alignments[index].0))); | 1828 | choice = Some(Choice::Command(Cmd::Align(alignments[index].0))); |
| 1753 | } | 1829 | } |
| 1754 | }); | 1830 | }); |
| 1755 | }); | 1831 | }); |
| 1756 | divider(ui, "text", theme); | 1832 | divider(ui, "text", theme); |
| 1757 | let tags: [(&[&str], NoteTag); 9] = [ | 1833 | let tags = NoteTag::ALL.map(|tag| (tag_icon(tag), tag)); |
| 1758 | ( | ||
| 1759 | tag_sources(TagIcon::CheckBox { checked: false }), | ||
| 1760 | NoteTag::ToDo, | ||
| 1761 | ), | ||
| 1762 | (tag_sources(TagIcon::Star), NoteTag::Important), | ||
| 1763 | (tag_sources(TagIcon::Question), NoteTag::Question), | ||
| 1764 | (art::TAG_REMEMBER, NoteTag::RememberForLater), | ||
| 1765 | (art::TAG_DEFINITION, NoteTag::Definition), | ||
| 1766 | (tag_sources(TagIcon::Highlight), NoteTag::Highlight), | ||
| 1767 | (tag_sources(TagIcon::Contact), NoteTag::Contact), | ||
| 1768 | (tag_sources(TagIcon::Address), NoteTag::Address), | ||
| 1769 | (tag_sources(TagIcon::Phone), NoteTag::PhoneNumber), | ||
| 1770 | ]; | ||
| 1771 | let all_tags = &tags; | ||
| 1772 | group(ui, "tags", |ui| { | 1834 | group(ui, "tags", |ui| { |
| 1773 | for (index, tags) in tags.chunks(5).enumerate() { | 1835 | // OneNote's first nine, Ctrl+1 to Ctrl+9, then the gallery's arrow. |
| 1836 | for (index, shown) in tags[..9].chunks(5).enumerate() { | ||
| 1774 | row(ui, index, |ui| { | 1837 | row(ui, index, |ui| { |
| 1775 | for (column, (icon, tag)) in tags.iter().enumerate() { | 1838 | for (column, (icon, tag)) in shown.iter().enumerate() { |
| 1776 | let lit = state.tags.contains(tag); | 1839 | let id = Cmd::Tag(*tag); |
| 1777 | if ui::shell::tool_button(ui, column, icon, [1.0; 4], lit).clicked { | 1840 | choice = |
| 1778 | choice = Some(Choice::Command(Cmd::Tag(*tag))); | 1841 | tool(ui, column, icon, [1.0; 4], id, status_of(id)).or(choice.take()); |
| 1779 | } | ||
| 1780 | } | 1842 | } |
| 1781 | if index == 1 { | 1843 | if index == 1 { |
| 1782 | let more = ui.id("more"); | 1844 | let more = ui.id("more"); |
| 1783 | let open = ui.popup_open(popup("tags")); | 1845 | let open = ui.popup_open(toolbar_popup("tags")); |
| 1784 | if ui::shell::tool_button(ui, "more", ui::shell::CHEVRON, text, open) | 1846 | if ui::shell::tool_button(ui, "more", ui::shell::CHEVRON, text, open) |
| 1785 | .pressed | 1847 | .pressed |
| 1786 | { | 1848 | { |
| 1787 | ui.open_popup(popup("tags")); | 1849 | ui.open_popup(toolbar_popup("tags")); |
| 1788 | } | 1850 | } |
| 1789 | let shortcuts: Vec<_> = all_tags | 1851 | let shortcuts: Vec<_> = tags |
| 1790 | .iter() | 1852 | .iter() |
| 1791 | .map(|(_, tag)| shortcut(Cmd::Tag(*tag))) | 1853 | .map(|(_, tag)| shortcut(Cmd::Tag(*tag))) |
| 1792 | .collect(); | 1854 | .collect(); |
| 1793 | let remove = shortcut(Cmd::RemoveTags); | 1855 | let remove = shortcut(Cmd::RemoveTags); |
| 1794 | let mut items: Vec<_> = all_tags | 1856 | let mut items: Vec<_> = tags |
| 1795 | .iter() | 1857 | .iter() |
| 1796 | .zip(&shortcuts) | 1858 | .zip(&shortcuts) |
| 1797 | .map(|((icon, tag), shortcut)| ui::popup::Item { | 1859 | .map(|((icon, tag), shortcut)| ui::popup::Item { |
| ... | @@ -1802,19 +1864,33 @@ impl State { | ... | @@ -1802,19 +1864,33 @@ impl State { |
| 1802 | ..Default::default() | 1864 | ..Default::default() |
| 1803 | }) | 1865 | }) |
| 1804 | .collect(); | 1866 | .collect(); |
| 1805 | items.push(ui::popup::Item { | 1867 | // Customize Tags awaits #8 and Find Tags its summary pane, #28. |
| 1806 | text: commands::command(Cmd::RemoveTags).title, | 1868 | items.extend([ |
| 1807 | shortcut: &remove, | 1869 | ui::popup::Item { |
| 1808 | separated: true, | 1870 | text: "Customize Tags…", |
| 1809 | ..Default::default() | 1871 | separated: true, |
| 1810 | }); | 1872 | disabled: true, |
| 1873 | ..Default::default() | ||
| 1874 | }, | ||
| 1875 | ui::popup::Item { | ||
| 1876 | text: commands::command(Cmd::RemoveTags).title, | ||
| 1877 | shortcut: &remove, | ||
| 1878 | ..Default::default() | ||
| 1879 | }, | ||
| 1880 | ui::popup::Item { | ||
| 1881 | text: "Find Tags", | ||
| 1882 | icon: Some(art::FIND_TAGS), | ||
| 1883 | separated: true, | ||
| 1884 | disabled: true, | ||
| 1885 | ..Default::default() | ||
| 1886 | }, | ||
| 1887 | ]); | ||
| 1811 | let anchor = ui::Anchor::Below(ui.rect(more).unwrap_or_default()); | 1888 | let anchor = ui::Anchor::Below(ui.rect(more).unwrap_or_default()); |
| 1812 | if let Some(chosen) = | 1889 | if let Some(chosen) = |
| 1813 | ui::popup::menu(ui, popup("tags"), anchor, &items, None) | 1890 | ui::popup::menu(ui, toolbar_popup("tags"), anchor, &items, None) |
| 1814 | { | 1891 | { |
| 1815 | choice = Some(Choice::Command( | 1892 | choice = Some(Choice::Command( |
| 1816 | all_tags | 1893 | tags.get(chosen) |
| 1817 | .get(chosen) | ||
| 1818 | .map_or(Cmd::RemoveTags, |(_, tag)| Cmd::Tag(*tag)), | 1894 | .map_or(Cmd::RemoveTags, |(_, tag)| Cmd::Tag(*tag)), |
| 1819 | )); | 1895 | )); |
| 1820 | } | 1896 | } |
| ... | @@ -1824,30 +1900,42 @@ impl State { | ... | @@ -1824,30 +1900,42 @@ impl State { |
| 1824 | }); | 1900 | }); |
| 1825 | divider(ui, "tags", theme); | 1901 | divider(ui, "tags", theme); |
| 1826 | group(ui, "insert", |ui| { | 1902 | group(ui, "insert", |ui| { |
| 1827 | for (index, buttons) in [ | 1903 | row(ui, 0, |ui| { |
| 1828 | &[ | 1904 | if status_of(Cmd::Table).enabled { |
| 1829 | ("table", art::TABLE, Cmd::Table), | 1905 | let anchor = |
| 1906 | ui::shell::menu_button(ui, "table", art::TABLE, toolbar_popup("table")); | ||
| 1907 | tip(ui, Cmd::Table); | ||
| 1908 | if let Some([columns, rows]) = | ||
| 1909 | ui::popup::table_picker(ui, toolbar_popup("table"), anchor, [10, 8]) | ||
| 1910 | { | ||
| 1911 | choice = Some(Choice::Table([rows, columns])); | ||
| 1912 | } | ||
| 1913 | } else { | ||
| 1914 | ui::shell::unavailable(ui, "table", art::TABLE, text, true); | ||
| 1915 | } | ||
| 1916 | for (part, icon, id) in [ | ||
| 1830 | ("picture", art::PICTURE, Cmd::Picture), | 1917 | ("picture", art::PICTURE, Cmd::Picture), |
| 1918 | ("clipping", art::SCREEN_CLIPPING, Cmd::ScreenClipping), | ||
| 1831 | ("file", art::ATTACHMENT, Cmd::Attachment), | 1919 | ("file", art::ATTACHMENT, Cmd::Attachment), |
| 1832 | ("link", art::LINK, Cmd::Link), | 1920 | ("link", art::LINK, Cmd::Link), |
| 1833 | ][..], | 1921 | ("space", art::INSERT_SPACE, Cmd::InsertSpace), |
| 1834 | &[ | 1922 | ] { |
| 1923 | choice = tool(ui, part, icon, text, id, status_of(id)).or(choice.take()); | ||
| 1924 | } | ||
| 1925 | }); | ||
| 1926 | row(ui, 1, |ui| { | ||
| 1927 | for (part, icon, id) in [ | ||
| 1835 | ("date", art::CALENDAR, Cmd::Date), | 1928 | ("date", art::CALENDAR, Cmd::Date), |
| 1836 | ("time", art::CLOCK, Cmd::Time), | 1929 | ("time", art::CLOCK, Cmd::Time), |
| 1930 | ("date and time", art::DATE_TIME, Cmd::DateTime), | ||
| 1837 | ("equation", art::EQUATION, Cmd::Equation), | 1931 | ("equation", art::EQUATION, Cmd::Equation), |
| 1838 | ], | 1932 | ("symbol", art::SYMBOL, Cmd::Symbol), |
| 1839 | ] | 1933 | ("audio", art::RECORD_AUDIO, Cmd::RecordAudio), |
| 1840 | .into_iter() | 1934 | ("video", art::RECORD_VIDEO, Cmd::RecordVideo), |
| 1841 | .enumerate() | 1935 | ] { |
| 1842 | { | 1936 | choice = tool(ui, part, icon, text, id, status_of(id)).or(choice.take()); |
| 1843 | row(ui, index, |ui| { | 1937 | } |
| 1844 | for (part, icon, id) in buttons { | 1938 | }); |
| 1845 | if ui::shell::tool_button(ui, part, icon, text, false).clicked { | ||
| 1846 | choice = Some(Choice::Command(*id)); | ||
| 1847 | } | ||
| 1848 | } | ||
| 1849 | }); | ||
| 1850 | } | ||
| 1851 | }); | 1939 | }); |
| 1852 | ui.leaf( | 1940 | ui.leaf( |
| 1853 | "space", | 1941 | "space", |
| ... | @@ -1856,25 +1944,93 @@ impl State { | ... | @@ -1856,25 +1944,93 @@ impl State { |
| 1856 | ..Spec::default() | 1944 | ..Spec::default() |
| 1857 | }, | 1945 | }, |
| 1858 | ); | 1946 | ); |
| 1859 | if let Some(status) = status { | 1947 | if let Some(session) = status { |
| 1860 | ui.leaf( | 1948 | sync::control(ui, session, theme, ui::shell::TOOL); |
| 1861 | "status", | ||
| 1862 | Spec { | ||
| 1863 | size: [fit(), px(ui::shell::TOOL)], | ||
| 1864 | text: Some(&status), | ||
| 1865 | color: Some(theme.text_dim), | ||
| 1866 | center: true, | ||
| 1867 | ..Spec::default() | ||
| 1868 | }, | ||
| 1869 | ); | ||
| 1870 | } | 1949 | } |
| 1871 | let zoom = self.view.zoom(); | 1950 | let zoom = self.view.zoom(); |
| 1872 | let label = format!("{:.0}%", zoom * 100.0); | 1951 | let label = format!("{:.0}%", zoom * 100.0); |
| 1873 | group(ui, "zoom", |ui| { | 1952 | group(ui, "view", |ui| { |
| 1874 | row(ui, 0, |ui| { | 1953 | row(ui, 0, |ui| { |
| 1875 | if ui::shell::tool_button(ui, "out", art::ZOOM_OUT, text, false).clicked { | 1954 | if status_of(Cmd::PageColor).enabled { |
| 1876 | choice = Some(Choice::Command(Cmd::ZoomOut)); | 1955 | let anchor = ui::shell::menu_button( |
| 1956 | ui, | ||
| 1957 | "page color", | ||
| 1958 | art::PAGE_COLOR, | ||
| 1959 | toolbar_popup("page color"), | ||
| 1960 | ); | ||
| 1961 | tip(ui, Cmd::PageColor); | ||
| 1962 | let swatches: Vec<_> = PAGE_COLORS | ||
| 1963 | .iter() | ||
| 1964 | .map(|(_, color)| colorref(*color)) | ||
| 1965 | .collect(); | ||
| 1966 | if let Some(chosen) = ui::popup::colors( | ||
| 1967 | ui, | ||
| 1968 | toolbar_popup("page color"), | ||
| 1969 | anchor, | ||
| 1970 | "No Color", | ||
| 1971 | &swatches, | ||
| 1972 | 4, | ||
| 1973 | ) { | ||
| 1974 | let index = chosen.and_then(|chosen| { | ||
| 1975 | swatches.iter().position(|swatch| *swatch == chosen) | ||
| 1976 | }); | ||
| 1977 | choice = Some(Choice::PageColor(index)); | ||
| 1978 | } | ||
| 1979 | } else { | ||
| 1980 | ui::shell::unavailable(ui, "page color", art::PAGE_COLOR, text, true); | ||
| 1981 | } | ||
| 1982 | // Nothing reads or writes rule lines yet. | ||
| 1983 | ui::shell::unavailable(ui, "rule lines", art::RULE_LINES, text, true); | ||
| 1984 | let full = Cmd::FullPageView; | ||
| 1985 | choice = tool( | ||
| 1986 | ui, | ||
| 1987 | "full page", | ||
| 1988 | art::FULL_PAGE_VIEW, | ||
| 1989 | text, | ||
| 1990 | full, | ||
| 1991 | status_of(full), | ||
| 1992 | ) | ||
| 1993 | .or(choice.take()); | ||
| 1994 | let more = ui.id("more"); | ||
| 1995 | let open = ui.popup_open(toolbar_popup("view")); | ||
| 1996 | if ui::shell::tool_button(ui, "more", ui::shell::CHEVRON, text, open).pressed { | ||
| 1997 | ui.open_popup(toolbar_popup("view")); | ||
| 1998 | } | ||
| 1999 | let views = [Cmd::Sidebar, Cmd::PageList, Cmd::DarkPages]; | ||
| 2000 | let mut items: Vec<_> = views | ||
| 2001 | .iter() | ||
| 2002 | .map(|id| ui::popup::Item { | ||
| 2003 | text: commands::command(*id).title, | ||
| 2004 | checked: status_of(*id).checked, | ||
| 2005 | disabled: !status_of(*id).enabled, | ||
| 2006 | ..Default::default() | ||
| 2007 | }) | ||
| 2008 | .collect(); | ||
| 2009 | // Spelling awaits a platform spell checker. | ||
| 2010 | items.push(ui::popup::Item { | ||
| 2011 | text: "Spelling", | ||
| 2012 | icon: Some(art::SPELLING), | ||
| 2013 | separated: true, | ||
| 2014 | disabled: true, | ||
| 2015 | ..Default::default() | ||
| 2016 | }); | ||
| 2017 | let anchor = ui::Anchor::Below(ui.rect(more).unwrap_or_default()); | ||
| 2018 | if let Some(chosen) = | ||
| 2019 | ui::popup::menu(ui, toolbar_popup("view"), anchor, &items, None) | ||
| 2020 | { | ||
| 2021 | choice = Some(Choice::Command(views[chosen])); | ||
| 1877 | } | 2022 | } |
| 2023 | }); | ||
| 2024 | row(ui, 1, |ui| { | ||
| 2025 | choice = tool( | ||
| 2026 | ui, | ||
| 2027 | "out", | ||
| 2028 | art::ZOOM_OUT, | ||
| 2029 | text, | ||
| 2030 | Cmd::ZoomOut, | ||
| 2031 | status_of(Cmd::ZoomOut), | ||
| 2032 | ) | ||
| 2033 | .or(choice.take()); | ||
| 1878 | let level = ui.leaf( | 2034 | let level = ui.leaf( |
| 1879 | "level", | 2035 | "level", |
| 1880 | Spec { | 2036 | Spec { |
| ... | @@ -1890,9 +2046,16 @@ impl State { | ... | @@ -1890,9 +2046,16 @@ impl State { |
| 1890 | if level.clicked { | 2046 | if level.clicked { |
| 1891 | choice = Some(Choice::Command(Cmd::ActualSize)); | 2047 | choice = Some(Choice::Command(Cmd::ActualSize)); |
| 1892 | } | 2048 | } |
| 1893 | if ui::shell::tool_button(ui, "in", art::ZOOM_IN, text, false).clicked { | 2049 | tip(ui, Cmd::ActualSize); |
| 1894 | choice = Some(Choice::Command(Cmd::ZoomIn)); | 2050 | choice = tool( |
| 1895 | } | 2051 | ui, |
| 2052 | "in", | ||
| 2053 | art::ZOOM_IN, | ||
| 2054 | text, | ||
| 2055 | Cmd::ZoomIn, | ||
| 2056 | status_of(Cmd::ZoomIn), | ||
| 2057 | ) | ||
| 2058 | .or(choice.take()); | ||
| 1896 | }); | 2059 | }); |
| 1897 | }); | 2060 | }); |
| 1898 | ui.close(); | 2061 | ui.close(); |
| ... | @@ -1919,6 +2082,7 @@ impl State { | ... | @@ -1919,6 +2082,7 @@ impl State { |
| 1919 | if ui::shell::tool_button(&mut self.ui, "new", art::PLUS, theme.text, false).clicked { | 2082 | if ui::shell::tool_button(&mut self.ui, "new", art::PLUS, theme.text, false).clicked { |
| 1920 | self.commands.push(Command::NewPage { under: None }); | 2083 | self.commands.push(Command::NewPage { under: None }); |
| 1921 | } | 2084 | } |
| 2085 | tip(&mut self.ui, commands::Id::NewPage); | ||
| 1922 | let toggle = if self.pages_open { | 2086 | let toggle = if self.pages_open { |
| 1923 | art::SIDEBAR_COLLAPSE | 2087 | art::SIDEBAR_COLLAPSE |
| 1924 | } else { | 2088 | } else { |
| ... | @@ -1927,6 +2091,7 @@ impl State { | ... | @@ -1927,6 +2091,7 @@ impl State { |
| 1927 | if ui::shell::tool_button(&mut self.ui, "toggle", toggle, theme.text, false).clicked { | 2091 | if ui::shell::tool_button(&mut self.ui, "toggle", toggle, theme.text, false).clicked { |
| 1928 | self.pages_open = !self.pages_open; | 2092 | self.pages_open = !self.pages_open; |
| 1929 | } | 2093 | } |
| 2094 | tip(&mut self.ui, commands::Id::PageList); | ||
| 1930 | self.ui.close(); | 2095 | self.ui.close(); |
| 1931 | } | 2096 | } |
| 1932 | 2097 | ||
| ... | @@ -1935,9 +2100,8 @@ impl State { | ... | @@ -1935,9 +2100,8 @@ impl State { |
| 1935 | fn page_list(&mut self, theme: &Theme, section: &ui::Section, tabs: Id) -> Option<Id> { | 2100 | fn page_list(&mut self, theme: &Theme, section: &ui::Section, tabs: Id) -> Option<Id> { |
| 1936 | let session = self.session.as_ref()?; | 2101 | let session = self.session.as_ref()?; |
| 1937 | let panel = self.ui.id("panel"); | 2102 | let panel = self.ui.id("panel"); |
| 1938 | let width = self | 2103 | let open = self.pages_open && !self.full_page; |
| 1939 | .ui | 2104 | let width = self.ui.animate(panel, if open { PAGE_LIST } else { 0.0 }); |
| 1940 | .animate(panel, if self.pages_open { PAGE_LIST } else { 0.0 }); | ||
| 1941 | self.ui.open( | 2105 | self.ui.open( |
| 1942 | "panel", | 2106 | "panel", |
| 1943 | Spec { | 2107 | Spec { |
| ... | @@ -2007,7 +2171,16 @@ impl State { | ... | @@ -2007,7 +2171,16 @@ impl State { |
| 2007 | if pressed { | 2171 | if pressed { |
| 2008 | self.view.pointer_pressed(at)? | 2172 | self.view.pointer_pressed(at)? |
| 2009 | } else { | 2173 | } else { |
| 2010 | self.view.pointer_released()? | 2174 | let response = self.view.pointer_released()?; |
| 2175 | let [anchor, focus] = self.view.editor.selection().positions; | ||
| 2176 | if anchor != focus | ||
| 2177 | && let Some(painted) = self.painter.take() | ||
| 2178 | { | ||
| 2179 | self.respond(response); | ||
| 2180 | self.format(canvas::editor::Formatting::Paint(painted))?; | ||
| 2181 | continue; | ||
| 2182 | } | ||
| 2183 | response | ||
| 2011 | } | 2184 | } |
| 2012 | } | 2185 | } |
| 2013 | ui::Event::Button { | 2186 | ui::Event::Button { |
| ... | @@ -2097,7 +2270,6 @@ impl State { | ... | @@ -2097,7 +2270,6 @@ impl State { |
| 2097 | Command::DeleteVersion { page, version } => { | 2270 | Command::DeleteVersion { page, version } => { |
| 2098 | let session = self.session.as_mut().ok_or("No section is open")?; | 2271 | let session = self.session.as_mut().ok_or("No section is open")?; |
| 2099 | session.section.delete_pages(&[version])?; | 2272 | session.section.delete_pages(&[version])?; |
| 2100 | session.status = "Saving"; | ||
| 2101 | session.refresh_conflicts()?; | 2273 | session.refresh_conflicts()?; |
| 2102 | self.commands.push(Command::OpenPage(page)); | 2274 | self.commands.push(Command::OpenPage(page)); |
| 2103 | } | 2275 | } |
| ... | @@ -2550,7 +2722,6 @@ impl State { | ... | @@ -2550,7 +2722,6 @@ impl State { |
| 2550 | .collect(), | 2722 | .collect(), |
| 2551 | }, | 2723 | }, |
| 2552 | )?; | 2724 | )?; |
| 2553 | session.status = "Saving"; | ||
| 2554 | // The page list shows the title as it is typed. | 2725 | // The page list shows the title as it is typed. |
| 2555 | if self.view.editor.active_outline().title { | 2726 | if self.view.editor.active_outline().title { |
| 2556 | session.pages = session.section.pages()?; | 2727 | session.pages = session.section.pages()?; |
| ... | @@ -2588,9 +2759,6 @@ impl State { | ... | @@ -2588,9 +2759,6 @@ impl State { |
| 2588 | let _ = self.view.editor.take_ops(); | 2759 | let _ = self.view.editor.take_ops(); |
| 2589 | self.view.editor.cancel_composition(&mut self.view.engine)?; | 2760 | self.view.editor.cancel_composition(&mut self.view.engine)?; |
| 2590 | platform::clear_marked_text(&self.window); | 2761 | platform::clear_marked_text(&self.window); |
| 2591 | if let Some(session) = &mut self.session { | ||
| 2592 | session.status = "Not saving"; | ||
| 2593 | } | ||
| 2594 | self.refresh()?; | 2762 | self.refresh()?; |
| 2595 | platform::alert( | 2763 | platform::alert( |
| 2596 | "Change not saved", | 2764 | "Change not saved", |
| ... | @@ -2604,13 +2772,13 @@ impl State { | ... | @@ -2604,13 +2772,13 @@ impl State { |
| 2604 | let Some(session) = &mut self.session else { | 2772 | let Some(session) = &mut self.session else { |
| 2605 | return Ok(()); | 2773 | return Ok(()); |
| 2606 | }; | 2774 | }; |
| 2607 | let shown = session.status; | 2775 | let shown = sync::label(&session.sync); |
| 2608 | let mut listed = false; | 2776 | let mut listed = false; |
| 2609 | let mut changed = false; | 2777 | let mut changed = false; |
| 2610 | let mut rejected = None; | 2778 | let mut rejected = None; |
| 2611 | for event in session.section.events() { | 2779 | for event in session.section.events() { |
| 2612 | use notebook::session::Event; | 2780 | use notebook::session::Event; |
| 2613 | session.status = match event { | 2781 | match event { |
| 2614 | Event::Changed(spaces) => { | 2782 | Event::Changed(spaces) => { |
| 2615 | listed = true; | 2783 | listed = true; |
| 2616 | changed |= spaces.contains(&session.space); | 2784 | changed |= spaces.contains(&session.space); |
| ... | @@ -2619,7 +2787,6 @@ impl State { | ... | @@ -2619,7 +2787,6 @@ impl State { |
| 2619 | session.section.replica(), | 2787 | session.section.replica(), |
| 2620 | spaces, | 2788 | spaces, |
| 2621 | ); | 2789 | ); |
| 2622 | continue; | ||
| 2623 | } | 2790 | } |
| 2624 | Event::Rejected { spaces, error } => { | 2791 | Event::Rejected { spaces, error } => { |
| 2625 | if spaces.contains(&session.space) { | 2792 | if spaces.contains(&session.space) { |
| ... | @@ -2627,21 +2794,13 @@ impl State { | ... | @@ -2627,21 +2794,13 @@ impl State { |
| 2627 | } else { | 2794 | } else { |
| 2628 | eprintln!("Saving failed: {error}"); | 2795 | eprintln!("Saving failed: {error}"); |
| 2629 | } | 2796 | } |
| 2630 | "Not saving" | ||
| 2631 | } | ||
| 2632 | Event::Attempt { | ||
| 2633 | status: notebook::EditStatus::Published { .. }, | ||
| 2634 | .. | ||
| 2635 | } => "Saved", | ||
| 2636 | Event::Attempt { .. } => "Saving", | ||
| 2637 | Event::Unreachable(_) => "Offline", | ||
| 2638 | Event::Failed(error) => { | ||
| 2639 | eprintln!("Synchronization stopped: {error}"); | ||
| 2640 | "Not saving" | ||
| 2641 | } | 2797 | } |
| 2642 | }; | 2798 | Event::Failed(error) => eprintln!("Synchronization stopped: {error}"), |
| 2799 | Event::Attempt { .. } | Event::Unreachable(_) => {} | ||
| 2800 | } | ||
| 2643 | } | 2801 | } |
| 2644 | if listed || changed || shown != session.status { | 2802 | session.sync = session.section.sync_status()?; |
| 2803 | if listed || changed || shown != sync::label(&session.sync) { | ||
| 2645 | self.window.request_redraw(); | 2804 | self.window.request_redraw(); |
| 2646 | } | 2805 | } |
| 2647 | if listed { | 2806 | if listed { |
| ... | @@ -2782,67 +2941,18 @@ impl State { | ... | @@ -2782,67 +2941,18 @@ impl State { |
| 2782 | if let Some(path) = self.snapshot.take() { | 2941 | if let Some(path) = self.snapshot.take() { |
| 2783 | self.snapshot(&path)?; | 2942 | self.snapshot(&path)?; |
| 2784 | } | 2943 | } |
| 2785 | if self.occluded || [self.config.width, self.config.height].contains(&0) { | 2944 | if self.occluded { |
| 2786 | return Ok(()); | 2945 | return Ok(()); |
| 2787 | } | 2946 | } |
| 2788 | let mut reconfigure = false; | 2947 | let Some(frame) = self.surface.frame(&self.renderer)? else { |
| 2789 | let frame = match self.surface.get_current_texture() { | 2948 | return Ok(()); |
| 2790 | wgpu::CurrentSurfaceTexture::Success(frame) => frame, | ||
| 2791 | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => { | ||
| 2792 | reconfigure = true; | ||
| 2793 | frame | ||
| 2794 | } | ||
| 2795 | wgpu::CurrentSurfaceTexture::Outdated => { | ||
| 2796 | self.surface.configure(&self.renderer.device, &self.config); | ||
| 2797 | self.window.request_redraw(); | ||
| 2798 | return Ok(()); | ||
| 2799 | } | ||
| 2800 | wgpu::CurrentSurfaceTexture::Lost => { | ||
| 2801 | self.surface = self.instance.create_surface(self.window.clone())?; | ||
| 2802 | platform::configure_presentation(&self.surface); | ||
| 2803 | self.surface.configure(&self.renderer.device, &self.config); | ||
| 2804 | self.window.request_redraw(); | ||
| 2805 | return Ok(()); | ||
| 2806 | } | ||
| 2807 | wgpu::CurrentSurfaceTexture::Timeout => { | ||
| 2808 | trace_input(&"Surface timeout"); | ||
| 2809 | return Ok(()); | ||
| 2810 | } | ||
| 2811 | wgpu::CurrentSurfaceTexture::Occluded => { | ||
| 2812 | trace_input(&"Surface occluded"); | ||
| 2813 | return Ok(()); | ||
| 2814 | } | ||
| 2815 | wgpu::CurrentSurfaceTexture::Validation => { | ||
| 2816 | return Err("Canvas surface validation failed".into()); | ||
| 2817 | } | ||
| 2818 | }; | 2949 | }; |
| 2819 | lap("acquire", start); | 2950 | lap("acquire", start); |
| 2820 | let size = [self.config.width, self.config.height]; | 2951 | self.paint(&frame.target)?; |
| 2821 | let target = self | ||
| 2822 | .translucent | ||
| 2823 | .as_mut() | ||
| 2824 | .map(|translucent| translucent.target(&self.renderer.device, size)); | ||
| 2825 | self.paint( | ||
| 2826 | target | ||
| 2827 | .as_ref() | ||
| 2828 | .unwrap_or(&frame.texture.create_view(&Default::default())), | ||
| 2829 | )?; | ||
| 2830 | if let Some(translucent) = &self.translucent { | ||
| 2831 | let window = frame.texture.create_view(&wgpu::TextureViewDescriptor { | ||
| 2832 | format: Some(self.config.format.remove_srgb_suffix()), | ||
| 2833 | ..Default::default() | ||
| 2834 | }); | ||
| 2835 | translucent.present(&self.renderer.device, &self.renderer.queue, &window); | ||
| 2836 | } | ||
| 2837 | self.window.pre_present_notify(); | ||
| 2838 | let start = Instant::now(); | 2952 | let start = Instant::now(); |
| 2839 | self.renderer.queue.present(frame); | 2953 | self.surface.present(&self.renderer, frame); |
| 2840 | platform::commit_presentation(&self.window); | ||
| 2841 | lap("present", start); | 2954 | lap("present", start); |
| 2842 | trace_input(&"Present submitted"); | 2955 | trace_input(&"Present submitted"); |
| 2843 | if reconfigure { | ||
| 2844 | self.surface.configure(&self.renderer.device, &self.config); | ||
| 2845 | } | ||
| 2846 | Ok(()) | 2956 | Ok(()) |
| 2847 | } | 2957 | } |
| 2848 | 2958 | ||
| ... | @@ -2850,7 +2960,7 @@ impl State { | ... | @@ -2850,7 +2960,7 @@ impl State { |
| 2850 | /// largest texture, so the surface renders scaled rather than failing. | 2960 | /// largest texture, so the surface renders scaled rather than failing. |
| 2851 | fn scale(&self) -> f32 { | 2961 | fn scale(&self) -> f32 { |
| 2852 | let size = self.window.inner_size(); | 2962 | let size = self.window.inner_size(); |
| 2853 | let most = self.renderer.device.limits().max_texture_dimension_2d as f32; | 2963 | let most = self.renderer.max_texture_dimension() as f32; |
| 2854 | let longest = size.width.max(size.height) as f32; | 2964 | let longest = size.width.max(size.height) as f32; |
| 2855 | self.window.scale_factor() as f32 * (most / longest).min(1.0) | 2965 | self.window.scale_factor() as f32 * (most / longest).min(1.0) |
| 2856 | } | 2966 | } |
| ... | @@ -2865,7 +2975,7 @@ impl State { | ... | @@ -2865,7 +2975,7 @@ impl State { |
| 2865 | } | 2975 | } |
| 2866 | 2976 | ||
| 2867 | /// Paints the interface with the page in its box. | 2977 | /// Paints the interface with the page in its box. |
| 2868 | fn paint(&mut self, target: &wgpu::TextureView) -> Result<(), Box<dyn Error>> { | 2978 | fn paint(&mut self, target: &draw::Target) -> Result<(), Box<dyn Error>> { |
| 2869 | let start = Instant::now(); | 2979 | let start = Instant::now(); |
| 2870 | let paper = self.paper(); | 2980 | let paper = self.paper(); |
| 2871 | self.view.update_pictures(paper, &self.redraw); | 2981 | self.view.update_pictures(paper, &self.redraw); |
| ... | @@ -2904,12 +3014,7 @@ impl State { | ... | @@ -2904,12 +3014,7 @@ impl State { |
| 2904 | trace_input(&("Draw", viewport.origin, viewport.scale)); | 3014 | trace_input(&("Draw", viewport.origin, viewport.scale)); |
| 2905 | let start = Instant::now(); | 3015 | let start = Instant::now(); |
| 2906 | self.renderer | 3016 | self.renderer |
| 2907 | .draw( | 3017 | .draw(target, self.surface.size, self.ui.theme.strip, &layers) |
| 2908 | target, | ||
| 2909 | [self.config.width, self.config.height], | ||
| 2910 | self.ui.theme.strip, | ||
| 2911 | &layers, | ||
| 2912 | ) | ||
| 2913 | .map_err(|error| format!("Canvas drawing failed: {error:?}"))?; | 3018 | .map_err(|error| format!("Canvas drawing failed: {error:?}"))?; |
| 2914 | lap("render", start); | 3019 | lap("render", start); |
| 2915 | Ok(()) | 3020 | Ok(()) |
| ... | @@ -2917,68 +3022,10 @@ impl State { | ... | @@ -2917,68 +3022,10 @@ impl State { |
| 2917 | 3022 | ||
| 2918 | /// Writes the frame to a PNG, so a covered window can be reviewed. | 3023 | /// Writes the frame to a PNG, so a covered window can be reviewed. |
| 2919 | fn snapshot(&mut self, path: &Path) -> Result<(), Box<dyn Error>> { | 3024 | fn snapshot(&mut self, path: &Path) -> Result<(), Box<dyn Error>> { |
| 2920 | let size = [self.config.width, self.config.height]; | 3025 | let size = self.surface.size; |
| 2921 | let device = &self.renderer.device; | 3026 | let offscreen = self.surface.offscreen(&self.renderer)?; |
| 2922 | let texture = device.create_texture(&wgpu::TextureDescriptor { | 3027 | self.paint(&offscreen.target)?; |
| 2923 | label: Some("Snapshot"), | 3028 | let pixels = self.surface.read(&self.renderer, offscreen)?; |
| 2924 | size: wgpu::Extent3d { | ||
| 2925 | width: size[0], | ||
| 2926 | height: size[1], | ||
| 2927 | depth_or_array_layers: 1, | ||
| 2928 | }, | ||
| 2929 | mip_level_count: 1, | ||
| 2930 | sample_count: 1, | ||
| 2931 | dimension: wgpu::TextureDimension::D2, | ||
| 2932 | format: self.config.format, | ||
| 2933 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, | ||
| 2934 | view_formats: &[], | ||
| 2935 | }); | ||
| 2936 | let row = (size[0] * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); | ||
| 2937 | let buffer = device.create_buffer(&wgpu::BufferDescriptor { | ||
| 2938 | label: Some("Snapshot readback"), | ||
| 2939 | size: u64::from(row) * u64::from(size[1]), | ||
| 2940 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, | ||
| 2941 | mapped_at_creation: false, | ||
| 2942 | }); | ||
| 2943 | self.paint(&texture.create_view(&Default::default()))?; | ||
| 2944 | let mut encoder = self | ||
| 2945 | .renderer | ||
| 2946 | .device | ||
| 2947 | .create_command_encoder(&Default::default()); | ||
| 2948 | encoder.copy_texture_to_buffer( | ||
| 2949 | texture.as_image_copy(), | ||
| 2950 | wgpu::TexelCopyBufferInfo { | ||
| 2951 | buffer: &buffer, | ||
| 2952 | layout: wgpu::TexelCopyBufferLayout { | ||
| 2953 | offset: 0, | ||
| 2954 | bytes_per_row: Some(row), | ||
| 2955 | rows_per_image: Some(size[1]), | ||
| 2956 | }, | ||
| 2957 | }, | ||
| 2958 | texture.size(), | ||
| 2959 | ); | ||
| 2960 | self.renderer.queue.submit([encoder.finish()]); | ||
| 2961 | buffer.map_async(wgpu::MapMode::Read, .., |_| {}); | ||
| 2962 | self.renderer.device.poll(wgpu::PollType::Wait { | ||
| 2963 | submission_index: None, | ||
| 2964 | timeout: Some(std::time::Duration::from_secs(5)), | ||
| 2965 | })?; | ||
| 2966 | let bgra = matches!( | ||
| 2967 | self.config.format, | ||
| 2968 | wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb | ||
| 2969 | ); | ||
| 2970 | let pixels: Vec<u8> = buffer | ||
| 2971 | .get_mapped_range(..)? | ||
| 2972 | .chunks_exact(row as usize) | ||
| 2973 | .flat_map(|line| line[..size[0] as usize * 4].chunks_exact(4)) | ||
| 2974 | .flat_map(|pixel| { | ||
| 2975 | if bgra { | ||
| 2976 | [pixel[2], pixel[1], pixel[0], pixel[3]] | ||
| 2977 | } else { | ||
| 2978 | [pixel[0], pixel[1], pixel[2], pixel[3]] | ||
| 2979 | } | ||
| 2980 | }) | ||
| 2981 | .collect(); | ||
| 2982 | let partial = path.with_extension("partial"); | 3029 | let partial = path.with_extension("partial"); |
| 2983 | let mut encoder = png::Encoder::new(std::fs::File::create(&partial)?, size[0], size[1]); | 3030 | let mut encoder = png::Encoder::new(std::fs::File::create(&partial)?, size[0], size[1]); |
| 2984 | encoder.set_color(png::ColorType::Rgba); | 3031 | encoder.set_color(png::ColorType::Rgba); |
| ... | @@ -3062,12 +3109,13 @@ impl State { | ... | @@ -3062,12 +3109,13 @@ impl State { |
| 3062 | } | 3109 | } |
| 3063 | 3110 | ||
| 3064 | /// The interface's colours in `appearance`, on white pages when `light_pages`, and over the | 3111 | /// The interface's colours in `appearance`, on white pages when `light_pages`, and over the |
| 3065 | /// system's material where it shows through as a `backdrop`. | 3112 | /// system's material where it shows through as a `backdrop`; menus as the desktop's. |
| 3066 | fn theme(appearance: winit::window::Theme, light_pages: bool, backdrop: bool) -> Theme { | 3113 | fn theme(appearance: winit::window::Theme, light_pages: bool, backdrop: bool) -> Theme { |
| 3067 | let mut theme = match appearance { | 3114 | let mut theme = match appearance { |
| 3068 | winit::window::Theme::Dark => Theme::dark(), | 3115 | winit::window::Theme::Dark => Theme::dark(), |
| 3069 | winit::window::Theme::Light => Theme::light(), | 3116 | winit::window::Theme::Light => Theme::light(), |
| 3070 | }; | 3117 | }; |
| 3118 | theme.desktop_menu = platform::menu(appearance); | ||
| 3071 | if light_pages { | 3119 | if light_pages { |
| 3072 | let light = Theme::light(); | 3120 | let light = Theme::light(); |
| 3073 | [theme.paper, theme.paper_ink] = [light.paper, light.paper_ink]; | 3121 | [theme.paper, theme.paper_ink] = [light.paper, light.paper_ink]; |
| ... | @@ -3667,6 +3715,47 @@ fn row(ui: &mut Ui, part: usize, buttons: impl FnOnce(&mut Ui)) { | ... | @@ -3667,6 +3715,47 @@ fn row(ui: &mut Ui, part: usize, buttons: impl FnOnce(&mut Ui)) { |
| 3667 | ui.close(); | 3715 | ui.close(); |
| 3668 | } | 3716 | } |
| 3669 | 3717 | ||
| 3718 | /// A tooltip naming command `id` and its chord on the box built last. It names the command, | ||
| 3719 | /// not a dialog it opens, so it leaves off the menu's ellipsis. | ||
| 3720 | /// A tool button for command `id`, lit while `status` checks it and faded while it does not | ||
| 3721 | /// apply; returns the command when clicked. | ||
| 3722 | fn tool( | ||
| 3723 | ui: &mut Ui, | ||
| 3724 | part: impl std::hash::Hash, | ||
| 3725 | icon: &'static [&'static str], | ||
| 3726 | tint: [f32; 4], | ||
| 3727 | id: commands::Id, | ||
| 3728 | status: commands::Status, | ||
| 3729 | ) -> Option<commands::Choice> { | ||
| 3730 | if !status.enabled { | ||
| 3731 | ui::shell::unavailable(ui, part, icon, tint, false); | ||
| 3732 | return None; | ||
| 3733 | } | ||
| 3734 | let clicked = ui::shell::tool_button(ui, part, icon, tint, status.checked).clicked; | ||
| 3735 | tip(ui, id); | ||
| 3736 | clicked.then_some(commands::Choice::Command(id)) | ||
| 3737 | } | ||
| 3738 | |||
| 3739 | /// The artwork the toolbar shows for `tag`. | ||
| 3740 | fn tag_icon(tag: canvas::editor::NoteTag) -> &'static [&'static str] { | ||
| 3741 | use canvas::editor::NoteTag; | ||
| 3742 | match tag { | ||
| 3743 | NoteTag::RememberForLater => art::TAG_REMEMBER, | ||
| 3744 | NoteTag::Definition => art::TAG_DEFINITION, | ||
| 3745 | tag => tag_sources(TagIcon::of(tag.shape(), false).unwrap_or(TagIcon::Other)), | ||
| 3746 | } | ||
| 3747 | } | ||
| 3748 | |||
| 3749 | /// The toolbar's popup `name`, which the menu bar opens too. | ||
| 3750 | fn toolbar_popup(name: &str) -> Id { | ||
| 3751 | Id::ROOT.child(("popup", name)) | ||
| 3752 | } | ||
| 3753 | |||
| 3754 | fn tip(ui: &mut Ui, id: commands::Id) { | ||
| 3755 | let title = commands::command(id).title.trim_end_matches('…'); | ||
| 3756 | ui::popup::tooltip(ui, title, &commands::shortcut(id), None); | ||
| 3757 | } | ||
| 3758 | |||
| 3670 | /// The line between two toolbar groups. | 3759 | /// The line between two toolbar groups. |
| 3671 | fn divider(ui: &mut Ui, part: &str, theme: &Theme) { | 3760 | fn divider(ui: &mut Ui, part: &str, theme: &Theme) { |
| 3672 | ui.leaf( | 3761 | ui.leaf( |
| ... | @@ -3701,6 +3790,7 @@ fn read_session( | ... | @@ -3701,6 +3790,7 @@ fn read_session( |
| 3701 | let page = section.page(space)?; | 3790 | let page = section.page(space)?; |
| 3702 | let conflicts = section.conflicts()?; | 3791 | let conflicts = section.conflicts()?; |
| 3703 | let history = section.versions()?; | 3792 | let history = section.versions()?; |
| 3793 | let sync = section.sync_status()?; | ||
| 3704 | Ok(( | 3794 | Ok(( |
| 3705 | Session { | 3795 | Session { |
| 3706 | section, | 3796 | section, |
| ... | @@ -3709,7 +3799,7 @@ fn read_session( | ... | @@ -3709,7 +3799,7 @@ fn read_session( |
| 3709 | tab, | 3799 | tab, |
| 3710 | pages, | 3800 | pages, |
| 3711 | space, | 3801 | space, |
| 3712 | status: "", | 3802 | sync, |
| 3713 | conflicts, | 3803 | conflicts, |
| 3714 | shown: None, | 3804 | shown: None, |
| 3715 | change: None, | 3805 | change: None, |
| ... | @@ -3792,6 +3882,15 @@ impl App { | ... | @@ -3792,6 +3882,15 @@ impl App { |
| 3792 | impl ApplicationHandler<UserEvent> for App { | 3882 | impl ApplicationHandler<UserEvent> for App { |
| 3793 | fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { | 3883 | fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { |
| 3794 | let event = match event { | 3884 | let event = match event { |
| 3885 | UserEvent::Picture(bytes) => { | ||
| 3886 | if let Some(state) = &mut self.state { | ||
| 3887 | if let Err(error) = state.insert_picture(bytes) { | ||
| 3888 | eprintln!("{error}"); | ||
| 3889 | } | ||
| 3890 | state.window.request_redraw(); | ||
| 3891 | } | ||
| 3892 | return; | ||
| 3893 | } | ||
| 3795 | UserEvent::InsertText(text) => { | 3894 | UserEvent::InsertText(text) => { |
| 3796 | if let Some(state) = &mut self.state { | 3895 | if let Some(state) = &mut self.state { |
| 3797 | if state.ui.focused() == Some(page()) { | 3896 | if state.ui.focused() == Some(page()) { |
| ... | @@ -3943,11 +4042,9 @@ impl ApplicationHandler<UserEvent> for App { | ... | @@ -3943,11 +4042,9 @@ impl ApplicationHandler<UserEvent> for App { |
| 3943 | WindowEvent::Resized(size) => { | 4042 | WindowEvent::Resized(size) => { |
| 3944 | if size.width > 0 && size.height > 0 { | 4043 | if size.width > 0 && size.height > 0 { |
| 3945 | let ratio = state.scale() / scale; | 4044 | let ratio = state.scale() / scale; |
| 3946 | state.config.width = (size.width as f32 * ratio) as u32; | 4045 | state.surface.size = |
| 3947 | state.config.height = (size.height as f32 * ratio) as u32; | 4046 | [size.width, size.height].map(|side| (side as f32 * ratio) as u32); |
| 3948 | state | 4047 | state.surface.configure(&state.renderer); |
| 3949 | .surface | ||
| 3950 | .configure(&state.renderer.device, &state.config); | ||
| 3951 | } | 4048 | } |
| 3952 | // Present inside AppKit's resize transaction; a redraw on the next turn | 4049 | // Present inside AppKit's resize transaction; a redraw on the next turn |
| 3953 | // lets the window show the previous frame at the new size. | 4050 | // lets the window show the previous frame at the new size. |
crates/snowbound/src/manage.rs+17-4| ... | @@ -116,7 +116,6 @@ impl State { | ... | @@ -116,7 +116,6 @@ impl State { |
| 116 | ops, | 116 | ops, |
| 117 | }, | 117 | }, |
| 118 | )?; | 118 | )?; |
| 119 | session.status = "Saving"; | ||
| 120 | self.title_focus = Some(space); | 119 | self.title_focus = Some(space); |
| 121 | self.commands.push(Command::OpenPage(space)); | 120 | self.commands.push(Command::OpenPage(space)); |
| 122 | Ok(()) | 121 | Ok(()) |
| ... | @@ -130,11 +129,26 @@ impl State { | ... | @@ -130,11 +129,26 @@ impl State { |
| 130 | ) -> Result<(), Box<dyn Error>> { | 129 | ) -> Result<(), Box<dyn Error>> { |
| 131 | self.persist()?; | 130 | self.persist()?; |
| 132 | let session = self.session.as_ref().ok_or("No section is open")?; | 131 | let session = self.session.as_ref().ok_or("No section is open")?; |
| 133 | let space = session.space; | 132 | let ops = template_ops(&session.section.page(session.space)?, choice)?; |
| 134 | let ops = template_ops(&session.section.page(space)?, choice)?; | 133 | self.edit_page(ops) |
| 134 | } | ||
| 135 | |||
| 136 | /// View, Page Color: gives the open page `color`, a COLORREF, or none; its art stays. | ||
| 137 | pub(crate) fn color_page(&mut self, color: Option<u32>) -> Result<(), Box<dyn Error>> { | ||
| 138 | if self.view.editor.page_color() == color { | ||
| 139 | return Ok(()); | ||
| 140 | } | ||
| 141 | self.persist()?; | ||
| 142 | self.edit_page(vec![PageOp::Color(color)]) | ||
| 143 | } | ||
| 144 | |||
| 145 | /// Applies `ops` to the open page as one edit and shows the result. | ||
| 146 | fn edit_page(&mut self, ops: Vec<PageOp>) -> Result<(), Box<dyn Error>> { | ||
| 135 | if ops.is_empty() { | 147 | if ops.is_empty() { |
| 136 | return Ok(()); | 148 | return Ok(()); |
| 137 | } | 149 | } |
| 150 | let session = self.session.as_ref().ok_or("No section is open")?; | ||
| 151 | let space = session.space; | ||
| 138 | session.section.apply( | 152 | session.section.apply( |
| 139 | &self.author, | 153 | &self.author, |
| 140 | Edit { | 154 | Edit { |
| ... | @@ -410,7 +424,6 @@ impl State { | ... | @@ -410,7 +424,6 @@ impl State { |
| 410 | ops: vec![Op::Section(SectionOp::Pages(edits))], | 424 | ops: vec![Op::Section(SectionOp::Pages(edits))], |
| 411 | }, | 425 | }, |
| 412 | )?; | 426 | )?; |
| 413 | session.status = "Saving"; | ||
| 414 | session.pages = session.section.pages()?; | 427 | session.pages = session.section.pages()?; |
| 415 | Ok(()) | 428 | Ok(()) |
| 416 | } | 429 | } |
crates/snowbound/src/menubar.rs+32| ... | @@ -74,6 +74,7 @@ const MENUS: &[Item] = &[ | ... | @@ -74,6 +74,7 @@ const MENUS: &[Item] = &[ |
| 74 | C(Id::Cut), | 74 | C(Id::Cut), |
| 75 | C(Id::Copy), | 75 | C(Id::Copy), |
| 76 | C(Id::Paste), | 76 | C(Id::Paste), |
| 77 | C(Id::FormatPainter), | ||
| 77 | C(Id::SelectAll), | 78 | C(Id::SelectAll), |
| 78 | S, | 79 | S, |
| 79 | C(Id::Find), | 80 | C(Id::Find), |
| ... | @@ -98,8 +99,11 @@ const MENUS: &[Item] = &[ | ... | @@ -98,8 +99,11 @@ const MENUS: &[Item] = &[ |
| 98 | C(Id::ZoomOut), | 99 | C(Id::ZoomOut), |
| 99 | C(Id::ActualSize), | 100 | C(Id::ActualSize), |
| 100 | S, | 101 | S, |
| 102 | C(Id::FullPageView), | ||
| 101 | C(Id::Sidebar), | 103 | C(Id::Sidebar), |
| 102 | C(Id::PageList), | 104 | C(Id::PageList), |
| 105 | S, | ||
| 106 | C(Id::PageColor), | ||
| 103 | C(Id::DarkPages), | 107 | C(Id::DarkPages), |
| 104 | ], | 108 | ], |
| 105 | ), | 109 | ), |
| ... | @@ -108,8 +112,11 @@ const MENUS: &[Item] = &[ | ... | @@ -108,8 +112,11 @@ const MENUS: &[Item] = &[ |
| 108 | &[ | 112 | &[ |
| 109 | C(Id::Table), | 113 | C(Id::Table), |
| 110 | C(Id::Picture), | 114 | C(Id::Picture), |
| 115 | C(Id::ScreenClipping), | ||
| 111 | C(Id::Link), | 116 | C(Id::Link), |
| 117 | S, | ||
| 112 | C(Id::Equation), | 118 | C(Id::Equation), |
| 119 | C(Id::Symbol), | ||
| 113 | S, | 120 | S, |
| 114 | C(Id::Date), | 121 | C(Id::Date), |
| 115 | C(Id::Time), | 122 | C(Id::Time), |
| ... | @@ -158,6 +165,31 @@ const MENUS: &[Item] = &[ | ... | @@ -158,6 +165,31 @@ const MENUS: &[Item] = &[ |
| 158 | C(Id::Tag(NoteTag::Contact)), | 165 | C(Id::Tag(NoteTag::Contact)), |
| 159 | C(Id::Tag(NoteTag::Address)), | 166 | C(Id::Tag(NoteTag::Address)), |
| 160 | C(Id::Tag(NoteTag::PhoneNumber)), | 167 | C(Id::Tag(NoteTag::PhoneNumber)), |
| 168 | Item::Menu( | ||
| 169 | "More Tags", | ||
| 170 | &[ | ||
| 171 | C(Id::Tag(NoteTag::WebSiteToVisit)), | ||
| 172 | C(Id::Tag(NoteTag::Idea)), | ||
| 173 | C(Id::Tag(NoteTag::Password)), | ||
| 174 | C(Id::Tag(NoteTag::Critical)), | ||
| 175 | C(Id::Tag(NoteTag::ProjectA)), | ||
| 176 | C(Id::Tag(NoteTag::ProjectB)), | ||
| 177 | C(Id::Tag(NoteTag::MovieToSee)), | ||
| 178 | C(Id::Tag(NoteTag::BookToRead)), | ||
| 179 | C(Id::Tag(NoteTag::MusicToListenTo)), | ||
| 180 | C(Id::Tag(NoteTag::SourceForArticle)), | ||
| 181 | C(Id::Tag(NoteTag::RememberForBlog)), | ||
| 182 | C(Id::Tag(NoteTag::DiscussWithPersonA)), | ||
| 183 | C(Id::Tag(NoteTag::DiscussWithPersonB)), | ||
| 184 | C(Id::Tag(NoteTag::DiscussWithManager)), | ||
| 185 | C(Id::Tag(NoteTag::SendInEmail)), | ||
| 186 | C(Id::Tag(NoteTag::ScheduleMeeting)), | ||
| 187 | C(Id::Tag(NoteTag::CallBack)), | ||
| 188 | C(Id::Tag(NoteTag::ToDoPriority1)), | ||
| 189 | C(Id::Tag(NoteTag::ToDoPriority2)), | ||
| 190 | C(Id::Tag(NoteTag::ClientRequest)), | ||
| 191 | ], | ||
| 192 | ), | ||
| 161 | S, | 193 | S, |
| 162 | C(Id::RemoveTags), | 194 | C(Id::RemoveTags), |
| 163 | ], | 195 | ], |
crates/snowbound/src/rename.rs-1| ... | @@ -193,7 +193,6 @@ impl State { | ... | @@ -193,7 +193,6 @@ impl State { |
| 193 | }], | 193 | }], |
| 194 | }, | 194 | }, |
| 195 | )?; | 195 | )?; |
| 196 | session.status = "Saving"; | ||
| 197 | session.pages = session.section.pages()?; | 196 | session.pages = session.section.pages()?; |
| 198 | self.edited(vec![space]); | 197 | self.edited(vec![space]); |
| 199 | self.refresh() | 198 | self.refresh() |
crates/snowbound/src/screenshot.rs+2-2| ... | @@ -28,9 +28,9 @@ impl State { | ... | @@ -28,9 +28,9 @@ impl State { |
| 28 | .ok_or("SNOWBOUND_SCREENSHOT_SIZE must be WIDTHxHEIGHT in points.")?, | 28 | .ok_or("SNOWBOUND_SCREENSHOT_SIZE must be WIDTHxHEIGHT in points.")?, |
| 29 | Err(_) => SIZE, | 29 | Err(_) => SIZE, |
| 30 | }; | 30 | }; |
| 31 | let most = self.renderer.device.limits().max_texture_dimension_2d as f32; | 31 | let most = self.renderer.max_texture_dimension() as f32; |
| 32 | let scale = SCALE.min(most / size[0].max(size[1])); | 32 | let scale = SCALE.min(most / size[0].max(size[1])); |
| 33 | [self.config.width, self.config.height] = size.map(|side| (side * scale).max(1.0) as u32); | 33 | self.surface.size = size.map(|side| (side * scale).max(1.0) as u32); |
| 34 | self.renderer.clear_glyph_cache(); | 34 | self.renderer.clear_glyph_cache(); |
| 35 | self.app_icon = platform::app_icon((16.0 * scale) as u32); | 35 | self.app_icon = platform::app_icon((16.0 * scale) as u32); |
| 36 | let response = self.view.scale_factor_changed(scale)?; | 36 | let response = self.view.scale_factor_changed(scale)?; |
crates/snowbound/src/search.rs+2-4| ... | @@ -788,9 +788,6 @@ impl State { | ... | @@ -788,9 +788,6 @@ impl State { |
| 788 | let [left, top, right, bottom] = rect; | 788 | let [left, top, right, bottom] = rect; |
| 789 | let anchor = Anchor::Over([left - 4.0, top - 4.0, right + 4.0, bottom + 4.0]); | 789 | let anchor = Anchor::Over([left - 4.0, top - 4.0, right + 4.0, bottom + 4.0]); |
| 790 | let width = RESULTS.max(right - left); | 790 | let width = RESULTS.max(right - left); |
| 791 | // The box takes the toolbar's box's place at once and widens as the dropdown comes in. | ||
| 792 | let open = self.ui.opening(results(), anchor).unwrap_or(1.0); | ||
| 793 | let field_width = right - left + (width - 8.0 - (right - left)) * open; | ||
| 794 | self.ui.open_as( | 791 | self.ui.open_as( |
| 795 | results(), | 792 | results(), |
| 796 | Spec { | 793 | Spec { |
| ... | @@ -809,8 +806,9 @@ impl State { | ... | @@ -809,8 +806,9 @@ impl State { |
| 809 | self.ui.open( | 806 | self.ui.open( |
| 810 | "box", | 807 | "box", |
| 811 | Spec { | 808 | Spec { |
| 809 | // Takes the toolbar's box's place at once, widening with the dropdown. | ||
| 812 | flags: Flags::STILL, | 810 | flags: Flags::STILL, |
| 813 | size: [px(field_width), px(ui::shell::TOOL)], | 811 | size: [fill(), px(ui::shell::TOOL)], |
| 814 | fill: Some(theme.base), | 812 | fill: Some(theme.base), |
| 815 | border: Some(theme.accent), | 813 | border: Some(theme.accent), |
| 816 | radius: 4.0, | 814 | radius: 4.0, |
crates/snowbound/src/sidebar.rs+6-1| ... | @@ -616,7 +616,11 @@ impl crate::State { | ... | @@ -616,7 +616,11 @@ impl crate::State { |
| 616 | } | 616 | } |
| 617 | let width = self.ui.animate( | 617 | let width = self.ui.animate( |
| 618 | self.ui.id("sidebar"), | 618 | self.ui.id("sidebar"), |
| 619 | if self.sidebar { WIDTH } else { 0.0 }, | 619 | if self.sidebar && !self.full_page { |
| 620 | WIDTH | ||
| 621 | } else { | ||
| 622 | 0.0 | ||
| 623 | }, | ||
| 620 | ); | 624 | ); |
| 621 | self.ui.open( | 625 | self.ui.open( |
| 622 | "sidebar", | 626 | "sidebar", |
| ... | @@ -735,6 +739,7 @@ impl crate::State { | ... | @@ -735,6 +739,7 @@ impl crate::State { |
| 735 | self.sidebar = !self.sidebar; | 739 | self.sidebar = !self.sidebar; |
| 736 | self.save_settings(); | 740 | self.save_settings(); |
| 737 | } | 741 | } |
| 742 | crate::tip(&mut self.ui, crate::commands::Id::Sidebar); | ||
| 738 | self.ui.close(); | 743 | self.ui.close(); |
| 739 | } | 744 | } |
| 740 | 745 |
crates/snowbound/src/surface.rs created+221| ... | @@ -0,0 +1,221 @@ | ||
| 1 | //! Frames reach the window through wgpu. | ||
| 2 | |||
| 3 | use crate::{platform, trace_input}; | ||
| 4 | use draw::Renderer; | ||
| 5 | use std::{error::Error, sync::Arc}; | ||
| 6 | use winit::window::Window; | ||
| 7 | |||
| 8 | pub struct Surface { | ||
| 9 | /// Device pixels frames and snapshots are drawn at; `configure` gives the window it. | ||
| 10 | pub size: [u32; 2], | ||
| 11 | window: Arc<Window>, | ||
| 12 | instance: wgpu::Instance, | ||
| 13 | surface: wgpu::Surface<'static>, | ||
| 14 | config: wgpu::SurfaceConfiguration, | ||
| 15 | /// Carries frames to the window where the system's backdrop shows through the strip. | ||
| 16 | translucent: Option<draw::Translucent>, | ||
| 17 | } | ||
| 18 | |||
| 19 | pub struct Frame { | ||
| 20 | pub target: wgpu::TextureView, | ||
| 21 | texture: wgpu::SurfaceTexture, | ||
| 22 | reconfigure: bool, | ||
| 23 | } | ||
| 24 | |||
| 25 | pub struct Offscreen { | ||
| 26 | pub target: wgpu::TextureView, | ||
| 27 | texture: wgpu::Texture, | ||
| 28 | } | ||
| 29 | |||
| 30 | impl Surface { | ||
| 31 | /// With `backdrop`, frames leave the system's material showing through their | ||
| 32 | /// transparent pixels. | ||
| 33 | pub async fn new( | ||
| 34 | window: Arc<Window>, | ||
| 35 | backdrop: bool, | ||
| 36 | ) -> Result<(Self, Renderer), Box<dyn Error>> { | ||
| 37 | let instance = wgpu::Instance::new( | ||
| 38 | wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(window.clone())), | ||
| 39 | ); | ||
| 40 | let surface = instance.create_surface(window.clone())?; | ||
| 41 | platform::configure_presentation(&surface); | ||
| 42 | let adapter = instance | ||
| 43 | .request_adapter(&wgpu::RequestAdapterOptions { | ||
| 44 | compatible_surface: Some(&surface), | ||
| 45 | ..Default::default() | ||
| 46 | }) | ||
| 47 | .await?; | ||
| 48 | let (device, queue) = adapter.request_device(&Default::default()).await?; | ||
| 49 | let size = window.inner_size(); | ||
| 50 | let mut config = surface | ||
| 51 | .get_default_config(&adapter, size.width, size.height) | ||
| 52 | .ok_or("No supported canvas surface")?; | ||
| 53 | let translucent = backdrop.then(|| { | ||
| 54 | // What wgpu calls post-multiplied is Core Animation's non-opaque layer, which | ||
| 55 | // composites colour premultiplied in sRGB, as `Translucent` leaves it. | ||
| 56 | config.alpha_mode = wgpu::CompositeAlphaMode::PostMultiplied; | ||
| 57 | let window_format = config.format.remove_srgb_suffix(); | ||
| 58 | config.view_formats.push(window_format); | ||
| 59 | draw::Translucent::new(&device, config.format, window_format) | ||
| 60 | }); | ||
| 61 | surface.configure(&device, &config); | ||
| 62 | eprintln!("Canvas GPU: {:?}", adapter.get_info()); | ||
| 63 | let renderer = Renderer::new(device, queue, config.format); | ||
| 64 | Ok(( | ||
| 65 | Self { | ||
| 66 | size: [config.width, config.height], | ||
| 67 | window, | ||
| 68 | instance, | ||
| 69 | surface, | ||
| 70 | config, | ||
| 71 | translucent, | ||
| 72 | }, | ||
| 73 | renderer, | ||
| 74 | )) | ||
| 75 | } | ||
| 76 | |||
| 77 | /// Whether frames leave the system's backdrop showing. | ||
| 78 | pub fn translucent(&self) -> bool { | ||
| 79 | self.translucent.is_some() | ||
| 80 | } | ||
| 81 | |||
| 82 | pub fn configure(&mut self, renderer: &Renderer) { | ||
| 83 | [self.config.width, self.config.height] = self.size; | ||
| 84 | self.surface.configure(&renderer.device, &self.config); | ||
| 85 | } | ||
| 86 | |||
| 87 | /// The window's next frame, or none to skip this one. | ||
| 88 | pub fn frame(&mut self, renderer: &Renderer) -> Result<Option<Frame>, Box<dyn Error>> { | ||
| 89 | if self.size.contains(&0) { | ||
| 90 | return Ok(None); | ||
| 91 | } | ||
| 92 | let (texture, reconfigure) = match self.surface.get_current_texture() { | ||
| 93 | wgpu::CurrentSurfaceTexture::Success(texture) => (texture, false), | ||
| 94 | wgpu::CurrentSurfaceTexture::Suboptimal(texture) => (texture, true), | ||
| 95 | wgpu::CurrentSurfaceTexture::Outdated => { | ||
| 96 | self.configure(renderer); | ||
| 97 | self.window.request_redraw(); | ||
| 98 | return Ok(None); | ||
| 99 | } | ||
| 100 | wgpu::CurrentSurfaceTexture::Lost => { | ||
| 101 | self.surface = self.instance.create_surface(self.window.clone())?; | ||
| 102 | platform::configure_presentation(&self.surface); | ||
| 103 | self.configure(renderer); | ||
| 104 | self.window.request_redraw(); | ||
| 105 | return Ok(None); | ||
| 106 | } | ||
| 107 | wgpu::CurrentSurfaceTexture::Timeout => { | ||
| 108 | trace_input(&"Surface timeout"); | ||
| 109 | return Ok(None); | ||
| 110 | } | ||
| 111 | wgpu::CurrentSurfaceTexture::Occluded => { | ||
| 112 | trace_input(&"Surface occluded"); | ||
| 113 | return Ok(None); | ||
| 114 | } | ||
| 115 | wgpu::CurrentSurfaceTexture::Validation => { | ||
| 116 | return Err("Canvas surface validation failed".into()); | ||
| 117 | } | ||
| 118 | }; | ||
| 119 | let target = match &mut self.translucent { | ||
| 120 | Some(translucent) => translucent.target(&renderer.device, self.size), | ||
| 121 | None => texture.texture.create_view(&Default::default()), | ||
| 122 | }; | ||
| 123 | Ok(Some(Frame { | ||
| 124 | target, | ||
| 125 | texture, | ||
| 126 | reconfigure, | ||
| 127 | })) | ||
| 128 | } | ||
| 129 | |||
| 130 | pub fn present(&mut self, renderer: &Renderer, frame: Frame) { | ||
| 131 | if let Some(translucent) = &self.translucent { | ||
| 132 | let window = frame | ||
| 133 | .texture | ||
| 134 | .texture | ||
| 135 | .create_view(&wgpu::TextureViewDescriptor { | ||
| 136 | format: Some(self.config.format.remove_srgb_suffix()), | ||
| 137 | ..Default::default() | ||
| 138 | }); | ||
| 139 | translucent.present(&renderer.device, &renderer.queue, &window); | ||
| 140 | } | ||
| 141 | self.window.pre_present_notify(); | ||
| 142 | renderer.queue.present(frame.texture); | ||
| 143 | platform::commit_presentation(&self.window); | ||
| 144 | if frame.reconfigure { | ||
| 145 | self.configure(renderer); | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | /// A target the size of the window's frames that `read` reads back. | ||
| 150 | pub fn offscreen(&self, renderer: &Renderer) -> Result<Offscreen, Box<dyn Error>> { | ||
| 151 | let texture = renderer.device.create_texture(&wgpu::TextureDescriptor { | ||
| 152 | label: Some("Snapshot"), | ||
| 153 | size: wgpu::Extent3d { | ||
| 154 | width: self.size[0], | ||
| 155 | height: self.size[1], | ||
| 156 | depth_or_array_layers: 1, | ||
| 157 | }, | ||
| 158 | mip_level_count: 1, | ||
| 159 | sample_count: 1, | ||
| 160 | dimension: wgpu::TextureDimension::D2, | ||
| 161 | format: self.config.format, | ||
| 162 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, | ||
| 163 | view_formats: &[], | ||
| 164 | }); | ||
| 165 | Ok(Offscreen { | ||
| 166 | target: texture.create_view(&Default::default()), | ||
| 167 | texture, | ||
| 168 | }) | ||
| 169 | } | ||
| 170 | |||
| 171 | /// The offscreen target's sRGB RGBA rows, top first. | ||
| 172 | pub fn read( | ||
| 173 | &self, | ||
| 174 | renderer: &Renderer, | ||
| 175 | offscreen: Offscreen, | ||
| 176 | ) -> Result<Vec<u8>, Box<dyn Error>> { | ||
| 177 | let size = self.size; | ||
| 178 | let row = (size[0] * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); | ||
| 179 | let buffer = renderer.device.create_buffer(&wgpu::BufferDescriptor { | ||
| 180 | label: Some("Snapshot readback"), | ||
| 181 | size: u64::from(row) * u64::from(size[1]), | ||
| 182 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, | ||
| 183 | mapped_at_creation: false, | ||
| 184 | }); | ||
| 185 | let mut encoder = renderer.device.create_command_encoder(&Default::default()); | ||
| 186 | encoder.copy_texture_to_buffer( | ||
| 187 | offscreen.texture.as_image_copy(), | ||
| 188 | wgpu::TexelCopyBufferInfo { | ||
| 189 | buffer: &buffer, | ||
| 190 | layout: wgpu::TexelCopyBufferLayout { | ||
| 191 | offset: 0, | ||
| 192 | bytes_per_row: Some(row), | ||
| 193 | rows_per_image: Some(size[1]), | ||
| 194 | }, | ||
| 195 | }, | ||
| 196 | offscreen.texture.size(), | ||
| 197 | ); | ||
| 198 | renderer.queue.submit([encoder.finish()]); | ||
| 199 | buffer.map_async(wgpu::MapMode::Read, .., |_| {}); | ||
| 200 | renderer.device.poll(wgpu::PollType::Wait { | ||
| 201 | submission_index: None, | ||
| 202 | timeout: Some(std::time::Duration::from_secs(5)), | ||
| 203 | })?; | ||
| 204 | let bgra = matches!( | ||
| 205 | self.config.format, | ||
| 206 | wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb | ||
| 207 | ); | ||
| 208 | Ok(buffer | ||
| 209 | .get_mapped_range(..)? | ||
| 210 | .chunks_exact(row as usize) | ||
| 211 | .flat_map(|line| line[..size[0] as usize * 4].chunks_exact(4)) | ||
| 212 | .flat_map(|pixel| { | ||
| 213 | if bgra { | ||
| 214 | [pixel[2], pixel[1], pixel[0], pixel[3]] | ||
| 215 | } else { | ||
| 216 | [pixel[0], pixel[1], pixel[2], pixel[3]] | ||
| 217 | } | ||
| 218 | }) | ||
| 219 | .collect()) | ||
| 220 | } | ||
| 221 | } | ||
crates/snowbound/src/surface_gl.rs created+114| ... | @@ -0,0 +1,114 @@ | ||
| 1 | //! Frames reach the window through an OpenGL context on its view, where wgpu has no | ||
| 2 | //! backend: Mac OS X 10.6. Frames draw into an sRGB target and are copied to the view. | ||
| 3 | |||
| 4 | use draw::{Renderer, Target}; | ||
| 5 | use objc2::{ | ||
| 6 | ffi::NSInteger, | ||
| 7 | msg_send, msg_send_id, | ||
| 8 | rc::{Allocated, Retained}, | ||
| 9 | runtime::{AnyClass, AnyObject}, | ||
| 10 | }; | ||
| 11 | use std::{error::Error, sync::Arc}; | ||
| 12 | use winit::{ | ||
| 13 | raw_window_handle::{HasWindowHandle, RawWindowHandle}, | ||
| 14 | window::Window, | ||
| 15 | }; | ||
| 16 | |||
| 17 | pub struct Surface { | ||
| 18 | /// Device pixels frames and snapshots are drawn at; `configure` gives the window it. | ||
| 19 | pub size: [u32; 2], | ||
| 20 | context: Retained<AnyObject>, | ||
| 21 | /// The frame target, kept between frames. | ||
| 22 | target: Option<Target>, | ||
| 23 | } | ||
| 24 | |||
| 25 | pub struct Frame { | ||
| 26 | pub target: Target, | ||
| 27 | } | ||
| 28 | |||
| 29 | pub type Offscreen = Frame; | ||
| 30 | |||
| 31 | impl Surface { | ||
| 32 | /// No backdrop shows through an OpenGL view. | ||
| 33 | pub async fn new( | ||
| 34 | window: Arc<Window>, | ||
| 35 | _backdrop: bool, | ||
| 36 | ) -> Result<(Self, Renderer), Box<dyn Error>> { | ||
| 37 | let RawWindowHandle::AppKit(handle) = window.window_handle()?.as_raw() else { | ||
| 38 | unreachable!() | ||
| 39 | }; | ||
| 40 | // NSOpenGLPFADoubleBuffer, NSOpenGLPFAAccelerated, NSOpenGLPFAColorSize 24, | ||
| 41 | // NSOpenGLPFAAlphaSize 8. | ||
| 42 | let attributes: [u32; 7] = [5, 73, 8, 24, 11, 8, 0]; | ||
| 43 | let context = unsafe { | ||
| 44 | let class = |name| AnyClass::get(name).expect("AppKit is linked"); | ||
| 45 | let format: Allocated<AnyObject> = msg_send_id![class("NSOpenGLPixelFormat"), alloc]; | ||
| 46 | let format: Option<Retained<AnyObject>> = | ||
| 47 | msg_send_id![format, initWithAttributes: attributes.as_ptr()]; | ||
| 48 | let format = format.ok_or("No accelerated OpenGL pixel format")?; | ||
| 49 | let context: Allocated<AnyObject> = msg_send_id![class("NSOpenGLContext"), alloc]; | ||
| 50 | let context: Option<Retained<AnyObject>> = msg_send_id![ | ||
| 51 | context, | ||
| 52 | initWithFormat: &*format, | ||
| 53 | shareContext: std::ptr::null::<AnyObject>() | ||
| 54 | ]; | ||
| 55 | let context = context.ok_or("No OpenGL context")?; | ||
| 56 | let view = handle.ns_view.as_ptr().cast::<AnyObject>(); | ||
| 57 | let _: () = msg_send![&context, setView: view]; | ||
| 58 | let _: () = msg_send![&context, makeCurrentContext]; | ||
| 59 | // NSOpenGLCPSwapInterval: present on the display's refresh. | ||
| 60 | let interval: i32 = 1; | ||
| 61 | let _: () = msg_send![&context, setValues: &interval, forParameter: 222 as NSInteger]; | ||
| 62 | context | ||
| 63 | }; | ||
| 64 | let renderer = Renderer::new()?; | ||
| 65 | eprintln!("Canvas GPU: OpenGL"); | ||
| 66 | let size = window.inner_size(); | ||
| 67 | Ok(( | ||
| 68 | Self { | ||
| 69 | size: [size.width, size.height], | ||
| 70 | context, | ||
| 71 | target: None, | ||
| 72 | }, | ||
| 73 | renderer, | ||
| 74 | )) | ||
| 75 | } | ||
| 76 | |||
| 77 | pub fn translucent(&self) -> bool { | ||
| 78 | false | ||
| 79 | } | ||
| 80 | |||
| 81 | pub fn configure(&mut self, _: &Renderer) { | ||
| 82 | let _: () = unsafe { msg_send![&self.context, update] }; | ||
| 83 | } | ||
| 84 | |||
| 85 | /// The window's next frame, or none to skip this one. | ||
| 86 | pub fn frame(&mut self, _: &Renderer) -> Result<Option<Frame>, Box<dyn Error>> { | ||
| 87 | if self.size.contains(&0) { | ||
| 88 | return Ok(None); | ||
| 89 | } | ||
| 90 | let target = match self.target.take() { | ||
| 91 | Some(target) if target.size() == self.size => target, | ||
| 92 | _ => Target::new(self.size)?, | ||
| 93 | }; | ||
| 94 | Ok(Some(Frame { target })) | ||
| 95 | } | ||
| 96 | |||
| 97 | pub fn present(&mut self, _: &Renderer, frame: Frame) { | ||
| 98 | frame.target.present(); | ||
| 99 | let _: () = unsafe { msg_send![&self.context, flushBuffer] }; | ||
| 100 | self.target = Some(frame.target); | ||
| 101 | } | ||
| 102 | |||
| 103 | /// A target the size of the window's frames that `read` reads back. | ||
| 104 | pub fn offscreen(&self, _: &Renderer) -> Result<Offscreen, Box<dyn Error>> { | ||
| 105 | Ok(Frame { | ||
| 106 | target: Target::new(self.size)?, | ||
| 107 | }) | ||
| 108 | } | ||
| 109 | |||
| 110 | /// The offscreen target's sRGB RGBA rows, top first. | ||
| 111 | pub fn read(&self, _: &Renderer, offscreen: Offscreen) -> Result<Vec<u8>, Box<dyn Error>> { | ||
| 112 | Ok(offscreen.target.read_pixels()) | ||
| 113 | } | ||
| 114 | } | ||
crates/snowbound/src/sync.rs created+224| ... | @@ -0,0 +1,224 @@ | ||
| 1 | //! The sync status at the top right, and the popup it opens: OneNote's Shared Notebook | ||
| 2 | //! Synchronization for the open section. | ||
| 3 | |||
| 4 | use crate::{Session, State, filetime, library, platform}; | ||
| 5 | use notebook::session::SyncStatus; | ||
| 6 | use std::{error::Error, io::ErrorKind}; | ||
| 7 | use ui::{Anchor, Axis, Flags, Id, Overflow, Spec, Theme, Ui, children, fill, fit, px}; | ||
| 8 | |||
| 9 | const WIDTH: f32 = 380.0; | ||
| 10 | |||
| 11 | fn id() -> Id { | ||
| 12 | Id::ROOT.child("sync-status") | ||
| 13 | } | ||
| 14 | |||
| 15 | fn button() -> Id { | ||
| 16 | Id::ROOT.child("sync-button") | ||
| 17 | } | ||
| 18 | |||
| 19 | /// The status's label, and what the reader can do about an error. | ||
| 20 | fn describe(sync: &SyncStatus) -> (&'static str, Option<&'static str>) { | ||
| 21 | use ErrorKind::*; | ||
| 22 | if library::offline() { | ||
| 23 | return ( | ||
| 24 | "Working offline", | ||
| 25 | Some("Changes stay on this computer until you sync."), | ||
| 26 | ); | ||
| 27 | } | ||
| 28 | match sync.error.as_ref().map(std::io::Error::kind) { | ||
| 29 | Some(PermissionDenied | ReadOnlyFilesystem) => ( | ||
| 30 | "Unable to sync", | ||
| 31 | Some( | ||
| 32 | "You can’t change this notebook where it’s stored. Changes stay on this computer.", | ||
| 33 | ), | ||
| 34 | ), | ||
| 35 | Some(WouldBlock | ResourceBusy) => ( | ||
| 36 | "Section in use", | ||
| 37 | Some("Someone else is saving this section. Sync continues when they finish."), | ||
| 38 | ), | ||
| 39 | Some( | ||
| 40 | NotFound | ConnectionRefused | ConnectionReset | ConnectionAborted | NotConnected | ||
| 41 | | TimedOut | HostUnreachable | NetworkUnreachable | NetworkDown | BrokenPipe | ||
| 42 | | AddrNotAvailable, | ||
| 43 | ) => ( | ||
| 44 | "Not connected", | ||
| 45 | Some("Changes stay on this computer and sync when the notebook is back."), | ||
| 46 | ), | ||
| 47 | Some(_) => ("Unable to sync", None), | ||
| 48 | None if sync.synced.is_none() || sync.queued > 0 => ("Syncing…", None), | ||
| 49 | None => ("Up to date", None), | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | pub(crate) fn label(sync: &SyncStatus) -> &'static str { | ||
| 54 | describe(sync).0 | ||
| 55 | } | ||
| 56 | |||
| 57 | /// A FILETIME as OneNote's "Last sync": the time, and the date too before today. | ||
| 58 | fn when(time: u64) -> String { | ||
| 59 | let [_, clock] = platform::date_text(time); | ||
| 60 | let day = platform::short_date(time); | ||
| 61 | if day == platform::short_date(filetime()) { | ||
| 62 | clock | ||
| 63 | } else { | ||
| 64 | format!("{day} {clock}") | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | /// The status beside the window's controls, which opens the popup. | ||
| 69 | pub(crate) fn control(ui: &mut Ui, session: &Session, theme: &Theme, height: f32) { | ||
| 70 | let strong = ui.popup_open(id()) || session.sync.error.is_some() && !library::offline(); | ||
| 71 | ui.open_as( | ||
| 72 | button(), | ||
| 73 | Spec { | ||
| 74 | flags: Flags::CLICKABLE, | ||
| 75 | size: [fit(), px(height)], | ||
| 76 | text: Some(label(&session.sync)), | ||
| 77 | color: Some(if strong { theme.text } else { theme.text_dim }), | ||
| 78 | hover_fill: Some(theme.hover()), | ||
| 79 | radius: 4.0, | ||
| 80 | pad: [8.0, 0.0], | ||
| 81 | center: true, | ||
| 82 | ..Spec::default() | ||
| 83 | }, | ||
| 84 | ); | ||
| 85 | ui.close(); | ||
| 86 | if ui.signal(button()).clicked { | ||
| 87 | if ui.popup_open(id()) { | ||
| 88 | ui.close_popup(id()); | ||
| 89 | } else { | ||
| 90 | ui.open_popup(id()); | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | impl State { | ||
| 96 | /// Builds the sync status popup while it is open. | ||
| 97 | pub(crate) fn sync_popup(&mut self) -> Result<(), Box<dyn Error>> { | ||
| 98 | let ui = &mut self.ui; | ||
| 99 | let Some(session) = &mut self.session else { | ||
| 100 | ui.close_popup(id()); | ||
| 101 | return Ok(()); | ||
| 102 | }; | ||
| 103 | if !ui.popup_open(id()) { | ||
| 104 | return Ok(()); | ||
| 105 | } | ||
| 106 | // Read afresh while shown, so the last sync time follows each poll. | ||
| 107 | session.sync = session.section.sync_status()?; | ||
| 108 | let theme = ui.theme.clone(); | ||
| 109 | let offline = library::offline(); | ||
| 110 | let (progress, advice) = describe(&session.sync); | ||
| 111 | ui.open_as( | ||
| 112 | id(), | ||
| 113 | Spec { | ||
| 114 | axis: Axis::Y, | ||
| 115 | size: [px(WIDTH), children()], | ||
| 116 | fill: Some(theme.popup), | ||
| 117 | border: Some(theme.chip), | ||
| 118 | shadow: Some(theme.shadow), | ||
| 119 | radius: 8.0, | ||
| 120 | pad: [14.0, 10.0], | ||
| 121 | gap: 2.0, | ||
| 122 | anchor: Some(Anchor::Below(ui.rect(button()).unwrap_or_default())), | ||
| 123 | ..Spec::default() | ||
| 124 | }, | ||
| 125 | ); | ||
| 126 | let text = |ui: &mut Ui, part: &str, text: &str, color: [f32; 4], bold: bool| { | ||
| 127 | ui.leaf( | ||
| 128 | part, | ||
| 129 | Spec { | ||
| 130 | size: [fill(), fit()], | ||
| 131 | text: Some(text), | ||
| 132 | color: Some(color), | ||
| 133 | bold, | ||
| 134 | overflow: Overflow::Wrap, | ||
| 135 | pad: [0.0, 3.0], | ||
| 136 | ..Spec::default() | ||
| 137 | }, | ||
| 138 | ); | ||
| 139 | }; | ||
| 140 | let row = |ui: &mut Ui, key: &str, value: &str| { | ||
| 141 | ui.open( | ||
| 142 | key, | ||
| 143 | Spec { | ||
| 144 | size: [fill(), children()], | ||
| 145 | gap: 8.0, | ||
| 146 | ..Spec::default() | ||
| 147 | }, | ||
| 148 | ); | ||
| 149 | ui.leaf( | ||
| 150 | "key", | ||
| 151 | Spec { | ||
| 152 | size: [px(92.0), px(theme.font_size * 1.6)], | ||
| 153 | text: Some(key), | ||
| 154 | color: Some(theme.text_dim), | ||
| 155 | ..Spec::default() | ||
| 156 | }, | ||
| 157 | ); | ||
| 158 | ui.leaf( | ||
| 159 | "value", | ||
| 160 | Spec { | ||
| 161 | size: [fill(), px(theme.font_size * 1.6)], | ||
| 162 | text: Some(value), | ||
| 163 | overflow: Overflow::Ellipsis, | ||
| 164 | ..Spec::default() | ||
| 165 | }, | ||
| 166 | ); | ||
| 167 | ui.close(); | ||
| 168 | }; | ||
| 169 | text(ui, "title", "Notebook Sync Status", theme.text, true); | ||
| 170 | row(ui, "Notebook", &session.library.name); | ||
| 171 | row(ui, "Location", &session.library.location); | ||
| 172 | row(ui, "Connection", &session.library.transport()); | ||
| 173 | row(ui, "Section", &session.tabs[session.tab].name); | ||
| 174 | row(ui, "Progress", progress); | ||
| 175 | if let Some(synced) = session.sync.synced { | ||
| 176 | row(ui, "Last sync", &when(synced)); | ||
| 177 | } | ||
| 178 | if session.sync.queued > 0 { | ||
| 179 | let queued = match session.sync.queued { | ||
| 180 | 1 => "1 change".to_owned(), | ||
| 181 | count => format!("{count} changes"), | ||
| 182 | }; | ||
| 183 | row(ui, "Not yet synced", &queued); | ||
| 184 | } | ||
| 185 | if let Some(advice) = advice { | ||
| 186 | text(ui, "advice", advice, theme.text, false); | ||
| 187 | } | ||
| 188 | if let Some(error) = &session.sync.error { | ||
| 189 | text(ui, "error", &error.to_string(), theme.text_dim, false); | ||
| 190 | } | ||
| 191 | if let Some(notice) = &session.library.notice { | ||
| 192 | let notice = format!("Snowbound’s SMB client couldn’t sign in: {notice}"); | ||
| 193 | text(ui, "notice", &notice, theme.text_dim, false); | ||
| 194 | } | ||
| 195 | ui.open( | ||
| 196 | "controls", | ||
| 197 | Spec { | ||
| 198 | size: [fill(), children()], | ||
| 199 | pad: [0.0, 6.0], | ||
| 200 | gap: 8.0, | ||
| 201 | ..Spec::default() | ||
| 202 | }, | ||
| 203 | ); | ||
| 204 | let toggled = ui::check_box(ui, "offline", "Work offline", offline).clicked; | ||
| 205 | ui.leaf( | ||
| 206 | "space", | ||
| 207 | Spec { | ||
| 208 | size: [fill(), px(1.0)], | ||
| 209 | ..Spec::default() | ||
| 210 | }, | ||
| 211 | ); | ||
| 212 | let now = ui::button(ui, "sync-now", "Sync Now").clicked; | ||
| 213 | ui.close(); | ||
| 214 | ui.close(); | ||
| 215 | if toggled { | ||
| 216 | library::set_offline(!offline); | ||
| 217 | session.section.set_offline(!offline); | ||
| 218 | } | ||
| 219 | if now { | ||
| 220 | session.section.wake(); | ||
| 221 | } | ||
| 222 | Ok(()) | ||
| 223 | } | ||
| 224 | } | ||
crates/ui/Cargo.toml+2-1| ... | @@ -5,12 +5,13 @@ edition = "2024" | ... | @@ -5,12 +5,13 @@ edition = "2024" |
| 5 | publish = false | 5 | publish = false |
| 6 | 6 | ||
| 7 | [dependencies] | 7 | [dependencies] |
| 8 | draw = { path = "../draw" } | 8 | draw = { path = "../draw", default-features = false, features = ["render"] } |
| 9 | nucleo-matcher = "0.3.1" | 9 | nucleo-matcher = "0.3.1" |
| 10 | parley.workspace = true | 10 | parley.workspace = true |
| 11 | winit = "0.30.13" | 11 | winit = "0.30.13" |
| 12 | 12 | ||
| 13 | [dev-dependencies] | 13 | [dev-dependencies] |
| 14 | draw = { path = "../draw", features = ["wgpu"] } | ||
| 14 | pollster = "0.4" | 15 | pollster = "0.4" |
| 15 | png = "0.18" | 16 | png = "0.18" |
| 16 | wgpu.workspace = true | 17 | wgpu.workspace = true |
crates/ui/src/layout.rs+14-3| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | use crate::{Axis, Built, Flags, ICON, ICON_GAP, Id, Overflow, Size, State, text::Texts}; | 1 | use crate::{Anchor, Axis, Built, Flags, ICON, ICON_GAP, Id, Overflow, Size, State, text::Texts}; |
| 2 | use std::collections::HashMap; | 2 | use std::collections::HashMap; |
| 3 | 3 | ||
| 4 | /// Sizes each box on both axes, then places it: standalone sizes, sizes taken from | 4 | /// Sizes each box on both axes, then places it: standalone sizes, sizes taken from |
| ... | @@ -19,7 +19,13 @@ pub(crate) fn solve( | ... | @@ -19,7 +19,13 @@ pub(crate) fn solve( |
| 19 | } | 19 | } |
| 20 | for node in nodes.iter_mut() { | 20 | for node in nodes.iter_mut() { |
| 21 | node.computed[axis] = match node.size[axis].size { | 21 | node.computed[axis] = match node.size[axis].size { |
| 22 | Size::Pixels(pixels) => pixels, | 22 | Size::Pixels(pixels) => match node.anchor { |
| 23 | Some(Anchor::Over(rect)) if axis == 0 => { | ||
| 24 | let from = rect[2] - rect[0]; | ||
| 25 | from + (pixels - from) * node.open | ||
| 26 | } | ||
| 27 | _ => pixels, | ||
| 28 | }, | ||
| 23 | Size::Text => { | 29 | Size::Text => { |
| 24 | let content = if axis == 0 { | 30 | let content = if axis == 0 { |
| 25 | node.content_width() | 31 | node.content_width() |
| ... | @@ -89,7 +95,12 @@ pub(crate) fn solve( | ... | @@ -89,7 +95,12 @@ pub(crate) fn solve( |
| 89 | let mut cursor = nodes[index].pad[axis]; | 95 | let mut cursor = nodes[index].pad[axis]; |
| 90 | for child in nodes[index].children.clone() { | 96 | for child in nodes[index].children.clone() { |
| 91 | nodes[child].relative[axis] = if let Some(anchor) = nodes[child].anchor { | 97 | nodes[child].relative[axis] = if let Some(anchor) = nodes[child].anchor { |
| 92 | anchor.place(axis, nodes[child].computed[axis], window) | 98 | let shown = nodes[child].computed[axis]; |
| 99 | let size = match nodes[child].size[axis].size { | ||
| 100 | Size::Pixels(pixels) => pixels, | ||
| 101 | _ => shown, | ||
| 102 | }; | ||
| 103 | anchor.place(axis, size, shown, window) | ||
| 93 | } else if nodes[child].flags.contains(Flags::FLOAT) { | 104 | } else if nodes[child].flags.contains(Flags::FLOAT) { |
| 94 | nodes[child].position[axis] | 105 | nodes[child].position[axis] |
| 95 | } else if along(&nodes[index], axis) { | 106 | } else if along(&nodes[index], axis) { |
crates/ui/src/lib.rs+152-36| ... | @@ -12,7 +12,7 @@ mod theme; | ... | @@ -12,7 +12,7 @@ mod theme; |
| 12 | mod widgets; | 12 | mod widgets; |
| 13 | 13 | ||
| 14 | pub use list::{List, Row, Rows, list}; | 14 | pub use list::{List, Row, Rows, list}; |
| 15 | pub use theme::{Section, Shades, Theme}; | 15 | pub use theme::{Menu, PopupMotion, Section, Shades, Shadow, Theme}; |
| 16 | pub use widgets::{button, check_box, edit_key, edit_modifiers, scrollbar, text_field}; | 16 | pub use widgets::{button, check_box, edit_key, edit_modifiers, scrollbar, text_field}; |
| 17 | 17 | ||
| 18 | use draw::{ | 18 | use draw::{ |
| ... | @@ -46,6 +46,12 @@ const ICON_GAP: f32 = 6.0; | ... | @@ -46,6 +46,12 @@ const ICON_GAP: f32 = 6.0; |
| 46 | /// Seconds a dialog and other popups take to open and to close. | 46 | /// Seconds a dialog and other popups take to open and to close. |
| 47 | const DIALOG: [f32; 2] = [0.24, 0.16]; | 47 | const DIALOG: [f32; 2] = [0.24, 0.16]; |
| 48 | const POPUP: [f32; 2] = [0.16, 0.12]; | 48 | const POPUP: [f32; 2] = [0.16, 0.12]; |
| 49 | /// Seconds the pointer rests on a control before its tooltip shows, as Windows' tooltips | ||
| 50 | /// wait a double-click interval; after one shows, others show at once for `TIP_WARM` | ||
| 51 | /// seconds, and one showing cold fades in over `TIP_FADE`. | ||
| 52 | const TIP_DELAY: f32 = 0.5; | ||
| 53 | const TIP_WARM: f32 = 0.5; | ||
| 54 | const TIP_FADE: f32 = 0.1; | ||
| 49 | 55 | ||
| 50 | /// A box's identity across frames: its parent's id combined with a builder-chosen part. | 56 | /// A box's identity across frames: its parent's id combined with a builder-chosen part. |
| 51 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | 57 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
| ... | @@ -200,8 +206,9 @@ pub enum Anchor { | ... | @@ -200,8 +206,9 @@ pub enum Anchor { |
| 200 | 206 | ||
| 201 | impl Anchor { | 207 | impl Anchor { |
| 202 | /// Where a popup `size` long on `axis` starts in a window `room` long: past the | 208 | /// Where a popup `size` long on `axis` starts in a window `room` long: past the |
| 203 | /// anchor on the axis it opens along, level with it otherwise. | 209 | /// anchor on the axis it opens along, level with it otherwise. Shown only `shown` long |
| 204 | fn place(self, axis: usize, size: f32, room: f32) -> f32 { | 210 | /// as it opens, it keeps the edge it would have at full size. |
| 211 | fn place(self, axis: usize, size: f32, shown: f32, room: f32) -> f32 { | ||
| 205 | let (rect, along) = match self { | 212 | let (rect, along) = match self { |
| 206 | Anchor::Below(rect) => (rect, Some(1)), | 213 | Anchor::Below(rect) => (rect, Some(1)), |
| 207 | Anchor::Right(rect) => (rect, Some(0)), | 214 | Anchor::Right(rect) => (rect, Some(0)), |
| ... | @@ -218,15 +225,16 @@ impl Anchor { | ... | @@ -218,15 +225,16 @@ impl Anchor { |
| 218 | if first + size <= room { | 225 | if first + size <= room { |
| 219 | first | 226 | first |
| 220 | } else if second >= 0.0 { | 227 | } else if second >= 0.0 { |
| 221 | second | 228 | second + size - shown |
| 222 | } else { | 229 | } else { |
| 223 | first.min(room - size).max(0.0) | 230 | first.min(room - size).max(0.0) + size - shown |
| 224 | } | 231 | } |
| 225 | } | 232 | } |
| 226 | 233 | ||
| 227 | /// How a popup laid out at `rect` beside the anchor shows `open` of the way open: it | 234 | /// How a popup laid out at `rect` beside the anchor shows `open` of the way open: it |
| 228 | /// grows out of the anchor as it fades in, and a dialog swings up into place. | 235 | /// grows out of the anchor as it fades in where it `grows`, and a dialog swings up into |
| 229 | fn motion(self, rect: [f32; 4], open: f32) -> Motion { | 236 | /// place. |
| 237 | fn motion(self, rect: [f32; 4], open: f32, grows: bool) -> Motion { | ||
| 230 | let pivot = match self { | 238 | let pivot = match self { |
| 231 | Anchor::Below(anchor) => [anchor[0], anchor[3]], | 239 | Anchor::Below(anchor) => [anchor[0], anchor[3]], |
| 232 | Anchor::Right(anchor) => [anchor[2], anchor[1]], | 240 | Anchor::Right(anchor) => [anchor[2], anchor[1]], |
| ... | @@ -235,8 +243,11 @@ impl Anchor { | ... | @@ -235,8 +243,11 @@ impl Anchor { |
| 235 | Anchor::Dialog => [(rect[0] + rect[2]) / 2.0, rect[1]], | 243 | Anchor::Dialog => [(rect[0] + rect[2]) / 2.0, rect[1]], |
| 236 | }; | 244 | }; |
| 237 | let (from, tilt) = match self { | 245 | let (from, tilt) = match self { |
| 246 | _ if !grows => (1.0, 0.0), | ||
| 238 | Anchor::Dialog => (0.95, 0.4), | 247 | Anchor::Dialog => (0.95, 0.4), |
| 239 | Anchor::Over(_) => (0.94, 0.25), | 248 | // Over a box, the popup widens out of it in layout instead of growing. |
| 249 | Anchor::Over(_) => (1.0, 0.2), | ||
| 250 | Anchor::Point(_) => (0.94, 0.2), | ||
| 240 | _ => (0.94, 0.0), | 251 | _ => (0.94, 0.0), |
| 241 | }; | 252 | }; |
| 242 | Motion { | 253 | Motion { |
| ... | @@ -290,7 +301,8 @@ pub struct Spec<'a> { | ... | @@ -290,7 +301,8 @@ pub struct Spec<'a> { |
| 290 | pub fill: Option<[f32; 4]>, | 301 | pub fill: Option<[f32; 4]>, |
| 291 | /// The fill's colour at the bottom, fading from `fill` at the top. | 302 | /// The fill's colour at the bottom, fading from `fill` at the top. |
| 292 | pub gradient: Option<[f32; 4]>, | 303 | pub gradient: Option<[f32; 4]>, |
| 293 | /// A soft shadow of the box's outline, painted beneath it. | 304 | /// A soft shadow of the box's outline, painted beneath it; a popup other than a dialog |
| 305 | /// casts the theme's menu shadows instead. | ||
| 294 | pub shadow: Option<[f32; 4]>, | 306 | pub shadow: Option<[f32; 4]>, |
| 295 | /// How far the box draws inside its leading, top, trailing and bottom edges: fill, | 307 | /// How far the box draws inside its leading, top, trailing and bottom edges: fill, |
| 296 | /// border, shadow, icon and label; the pointer still finds the whole box. | 308 | /// border, shadow, icon and label; the pointer still finds the whole box. |
| ... | @@ -445,6 +457,8 @@ struct Built { | ... | @@ -445,6 +457,8 @@ struct Built { |
| 445 | offset: [f32; 2], | 457 | offset: [f32; 2], |
| 446 | cursor: Option<CursorIcon>, | 458 | cursor: Option<CursorIcon>, |
| 447 | anchor: Option<Anchor>, | 459 | anchor: Option<Anchor>, |
| 460 | /// How far open a popup over a box is, as it widens out of the box. | ||
| 461 | open: f32, | ||
| 448 | motion: Option<Motion>, | 462 | motion: Option<Motion>, |
| 449 | /// Rectangles relative to the box, painted over its fill. | 463 | /// Rectangles relative to the box, painted over its fill. |
| 450 | marks: Vec<([f32; 4], [f32; 4], f32)>, | 464 | marks: Vec<([f32; 4], [f32; 4], f32)>, |
| ... | @@ -489,6 +503,15 @@ struct Popup { | ... | @@ -489,6 +503,15 @@ struct Popup { |
| 489 | opened: Instant, | 503 | opened: Instant, |
| 490 | } | 504 | } |
| 491 | 505 | ||
| 506 | /// The tooltip of the box under the pointer, the last frame it was built, and when it | ||
| 507 | /// shows: never while None, after a press or the wheel, until the pointer leaves the box. | ||
| 508 | #[derive(Clone, Copy)] | ||
| 509 | struct Tip { | ||
| 510 | id: Id, | ||
| 511 | frame: u64, | ||
| 512 | due: Option<Instant>, | ||
| 513 | } | ||
| 514 | |||
| 492 | /// A popup's painting as it last showed, fading out since it closed. | 515 | /// A popup's painting as it last showed, fading out since it closed. |
| 493 | struct Closing { | 516 | struct Closing { |
| 494 | id: Id, | 517 | id: Id, |
| ... | @@ -591,6 +614,9 @@ pub struct Ui { | ... | @@ -591,6 +614,9 @@ pub struct Ui { |
| 591 | /// `popups_painted`. | 614 | /// `popups_painted`. |
| 592 | painted: Vec<(Id, Anchor, [f32; 4], std::ops::Range<usize>)>, | 615 | painted: Vec<(Id, Anchor, [f32; 4], std::ops::Range<usize>)>, |
| 593 | closing: Vec<Closing>, | 616 | closing: Vec<Closing>, |
| 617 | tip: Option<Tip>, | ||
| 618 | /// When a tooltip last showed. | ||
| 619 | warm: Option<Instant>, | ||
| 594 | animating: bool, | 620 | animating: bool, |
| 595 | /// This frame routed input, whose effects the builder may only have seen after boxes | 621 | /// This frame routed input, whose effects the builder may only have seen after boxes |
| 596 | /// built before it read their state; one more frame shows them. | 622 | /// built before it read their state; one more frame shows them. |
| ... | @@ -633,6 +659,8 @@ impl Ui { | ... | @@ -633,6 +659,8 @@ impl Ui { |
| 633 | popups_painted: 0, | 659 | popups_painted: 0, |
| 634 | painted: Vec::new(), | 660 | painted: Vec::new(), |
| 635 | closing: Vec::new(), | 661 | closing: Vec::new(), |
| 662 | tip: None, | ||
| 663 | warm: None, | ||
| 636 | animating: false, | 664 | animating: false, |
| 637 | routed: false, | 665 | routed: false, |
| 638 | caret: None, | 666 | caret: None, |
| ... | @@ -742,6 +770,12 @@ impl Ui { | ... | @@ -742,6 +770,12 @@ impl Ui { |
| 742 | } | 770 | } |
| 743 | 771 | ||
| 744 | fn route(&mut self, event: Event) { | 772 | fn route(&mut self, event: Event) { |
| 773 | if matches!(event, Event::Button { pressed: true, .. } | Event::Wheel(_)) { | ||
| 774 | if let Some(tip) = &mut self.tip { | ||
| 775 | tip.due = None; | ||
| 776 | } | ||
| 777 | self.warm = None; | ||
| 778 | } | ||
| 745 | match event { | 779 | match event { |
| 746 | Event::PointerMoved(point) => { | 780 | Event::PointerMoved(point) => { |
| 747 | self.pointer = Some(point); | 781 | self.pointer = Some(point); |
| ... | @@ -997,10 +1031,18 @@ impl Ui { | ... | @@ -997,10 +1031,18 @@ impl Ui { |
| 997 | } else { | 1031 | } else { |
| 998 | *self.stack.last().unwrap() | 1032 | *self.stack.last().unwrap() |
| 999 | }; | 1033 | }; |
| 1034 | let open = match spec.anchor { | ||
| 1035 | Some(anchor @ Anchor::Over(_)) if self.popup_motion(anchor) == PopupMotion::Grow => { | ||
| 1036 | self.opening(id, anchor).unwrap_or(1.0) | ||
| 1037 | } | ||
| 1038 | _ => 1.0, | ||
| 1039 | }; | ||
| 1000 | let index = self.nodes.len(); | 1040 | let index = self.nodes.len(); |
| 1001 | self.states.entry(id).or_default().touched = self.frame; | 1041 | self.states.entry(id).or_default().touched = self.frame; |
| 1002 | self.nodes | 1042 | self.nodes.push(Built { |
| 1003 | .push(Built::new(id, parent, spec, label, self.theme.text)); | 1043 | open, |
| 1044 | ..Built::new(id, parent, spec, label, self.theme.text) | ||
| 1045 | }); | ||
| 1004 | self.nodes[parent].children.push(index); | 1046 | self.nodes[parent].children.push(index); |
| 1005 | self.stack.push(index); | 1047 | self.stack.push(index); |
| 1006 | id | 1048 | id |
| ... | @@ -1116,6 +1158,13 @@ impl Ui { | ... | @@ -1116,6 +1158,13 @@ impl Ui { |
| 1116 | { | 1158 | { |
| 1117 | self.close_from(gone); | 1159 | self.close_from(gone); |
| 1118 | } | 1160 | } |
| 1161 | if let Some(tip) = self.tip.filter(|tip| tip.frame != self.frame) { | ||
| 1162 | if tip.due.is_some_and(|due| due <= self.now) { | ||
| 1163 | self.warm = Some(self.now); | ||
| 1164 | } | ||
| 1165 | self.tip = None; | ||
| 1166 | } | ||
| 1167 | let tip = self.tip.map(|tip| tip.id.child("tooltip")); | ||
| 1119 | let last = std::mem::take(&mut self.display); | 1168 | let last = std::mem::take(&mut self.display); |
| 1120 | let last_popups = self.popups_painted; | 1169 | let last_popups = self.popups_painted; |
| 1121 | self.hits.clear(); | 1170 | self.hits.clear(); |
| ... | @@ -1129,9 +1178,13 @@ impl Ui { | ... | @@ -1129,9 +1178,13 @@ impl Ui { |
| 1129 | continue; | 1178 | continue; |
| 1130 | }; | 1179 | }; |
| 1131 | let (id, rect) = (node.id, node.rect); | 1180 | let (id, rect) = (node.id, node.rect); |
| 1181 | if Some(id) == tip { | ||
| 1182 | continue; | ||
| 1183 | } | ||
| 1132 | if let Some(open) = self.opening(id, anchor) { | 1184 | if let Some(open) = self.opening(id, anchor) { |
| 1133 | self.scrim(anchor, open); | 1185 | self.scrim(anchor, open); |
| 1134 | self.nodes[index].motion = Some(anchor.motion(rect, open)); | 1186 | let grows = self.popup_motion(anchor) == PopupMotion::Grow; |
| 1187 | self.nodes[index].motion = Some(anchor.motion(rect, open, grows)); | ||
| 1135 | } | 1188 | } |
| 1136 | let from = self.display.len() - self.popups_painted; | 1189 | let from = self.display.len() - self.popups_painted; |
| 1137 | self.paint(index, None, None); | 1190 | self.paint(index, None, None); |
| ... | @@ -1167,23 +1220,47 @@ impl Ui { | ... | @@ -1167,23 +1220,47 @@ impl Ui { |
| 1167 | closed: self.now, | 1220 | closed: self.now, |
| 1168 | }); | 1221 | }); |
| 1169 | } | 1222 | } |
| 1170 | let closing = std::mem::take(&mut self.closing); | 1223 | for closing in std::mem::take(&mut self.closing) { |
| 1171 | for closing in &closing { | 1224 | let motion = self.popup_motion(closing.anchor); |
| 1172 | let shut = self.progress(closing.closed, closing.anchor.durations()[1]); | 1225 | let open = match motion { |
| 1173 | let open = (1.0 - shut).powi(3); | 1226 | PopupMotion::Grow => { |
| 1227 | (1.0 - self.progress(closing.closed, closing.anchor.durations()[1])).powi(3) | ||
| 1228 | } | ||
| 1229 | PopupMotion::Cut => 0.0, | ||
| 1230 | PopupMotion::Fade([_, close]) => { | ||
| 1231 | (1.0 - self.progress(closing.closed, close)).powi(4) | ||
| 1232 | } | ||
| 1233 | }; | ||
| 1234 | if open == 0.0 { | ||
| 1235 | continue; | ||
| 1236 | } | ||
| 1174 | self.scrim(closing.anchor, open); | 1237 | self.scrim(closing.anchor, open); |
| 1175 | let motion = closing.anchor.motion(closing.rect, open); | 1238 | let grows = motion == PopupMotion::Grow; |
| 1239 | let motion = closing.anchor.motion(closing.rect, open, grows); | ||
| 1176 | self.display.push(Display::Motion(Some(motion))); | 1240 | self.display.push(Display::Motion(Some(motion))); |
| 1177 | self.display.extend(closing.display.iter().cloned()); | 1241 | self.display.extend(closing.display.iter().cloned()); |
| 1178 | self.display.push(Display::Motion(None)); | 1242 | self.display.push(Display::Motion(None)); |
| 1243 | if !self.painted.iter().any(|(shown, ..)| *shown == closing.id) { | ||
| 1244 | self.closing.push(closing); | ||
| 1245 | } | ||
| 1246 | } | ||
| 1247 | // Over everything, closing popups included; it goes at once, without closing. | ||
| 1248 | if let Some(index) = self.nodes[0] | ||
| 1249 | .children | ||
| 1250 | .iter() | ||
| 1251 | .copied() | ||
| 1252 | .find(|index| Some(self.nodes[*index].id) == tip) | ||
| 1253 | && let Some(due) = self.tip.and_then(|tip| tip.due) | ||
| 1254 | { | ||
| 1255 | let opacity = self.progress(due, TIP_FADE); | ||
| 1256 | self.nodes[index].motion = Some(Motion { | ||
| 1257 | zoom: 1.0, | ||
| 1258 | pivot: [0.0; 2], | ||
| 1259 | tilt: 0.0, | ||
| 1260 | opacity, | ||
| 1261 | }); | ||
| 1262 | self.paint(index, None, None); | ||
| 1179 | } | 1263 | } |
| 1180 | self.closing = closing | ||
| 1181 | .into_iter() | ||
| 1182 | .filter(|closing| { | ||
| 1183 | self.progress(closing.closed, closing.anchor.durations()[1]) < 1.0 | ||
| 1184 | && !self.painted.iter().any(|(shown, ..)| *shown == closing.id) | ||
| 1185 | }) | ||
| 1186 | .collect(); | ||
| 1187 | self.modal = if self.popups.is_empty() { 0 } else { beneath }; | 1264 | self.modal = if self.popups.is_empty() { 0 } else { beneath }; |
| 1188 | for id in [&mut self.hover, &mut self.active, &mut self.focus] { | 1265 | for id in [&mut self.hover, &mut self.active, &mut self.focus] { |
| 1189 | if id.is_some_and(|id| !self.states.contains_key(&id)) { | 1266 | if id.is_some_and(|id| !self.states.contains_key(&id)) { |
| ... | @@ -1195,7 +1272,20 @@ impl Ui { | ... | @@ -1195,7 +1272,20 @@ impl Ui { |
| 1195 | /// How far open popup `id` beside `anchor` shows this frame, from 0 to 1, while open. | 1272 | /// How far open popup `id` beside `anchor` shows this frame, from 0 to 1, while open. |
| 1196 | pub fn opening(&mut self, id: Id, anchor: Anchor) -> Option<f32> { | 1273 | pub fn opening(&mut self, id: Id, anchor: Anchor) -> Option<f32> { |
| 1197 | let opened = self.popups.iter().find(|popup| popup.id == id)?.opened; | 1274 | let opened = self.popups.iter().find(|popup| popup.id == id)?.opened; |
| 1198 | Some(1.0 - (1.0 - self.progress(opened, anchor.durations()[0])).powi(3)) | 1275 | Some(match self.popup_motion(anchor) { |
| 1276 | PopupMotion::Grow => 1.0 - (1.0 - self.progress(opened, anchor.durations()[0])).powi(3), | ||
| 1277 | PopupMotion::Cut => 1.0, | ||
| 1278 | PopupMotion::Fade([open, _]) => self.progress(opened, open), | ||
| 1279 | }) | ||
| 1280 | } | ||
| 1281 | |||
| 1282 | /// How popups beside `anchor` open and close; dialogs always swing as Snowbound's own. | ||
| 1283 | fn popup_motion(&self, anchor: Anchor) -> PopupMotion { | ||
| 1284 | if anchor == Anchor::Dialog { | ||
| 1285 | PopupMotion::Grow | ||
| 1286 | } else { | ||
| 1287 | self.theme.menu().motion | ||
| 1288 | } | ||
| 1199 | } | 1289 | } |
| 1200 | 1290 | ||
| 1201 | /// How far through an animation `duration` seconds long that began at `start` this | 1291 | /// How far through an animation `duration` seconds long that began at `start` this |
| ... | @@ -1255,18 +1345,43 @@ impl Ui { | ... | @@ -1255,18 +1345,43 @@ impl Ui { |
| 1255 | rect[3] - bottom, | 1345 | rect[3] - bottom, |
| 1256 | ]; | 1346 | ]; |
| 1257 | let size = [painted[2] - painted[0], painted[3] - painted[1]]; | 1347 | let size = [painted[2] - painted[0], painted[3] - painted[1]]; |
| 1258 | if let Some(shadow) = node.shadow { | 1348 | if let Some(color) = node.shadow { |
| 1259 | let [spread, drop] = if node.anchor.is_some() { | 1349 | let (own, menu); |
| 1260 | POPUP_SHADOW | 1350 | let shadows: &[Shadow] = match node.anchor { |
| 1261 | } else { | 1351 | Some(Anchor::Dialog) | None => { |
| 1262 | SHADOW | 1352 | let [blur, drop] = if node.anchor.is_some() { |
| 1353 | POPUP_SHADOW | ||
| 1354 | } else { | ||
| 1355 | SHADOW | ||
| 1356 | }; | ||
| 1357 | own = [Shadow { | ||
| 1358 | color, | ||
| 1359 | blur, | ||
| 1360 | drop, | ||
| 1361 | spread: 0.0, | ||
| 1362 | }]; | ||
| 1363 | &own | ||
| 1364 | } | ||
| 1365 | Some(_) => { | ||
| 1366 | menu = self.theme.menu(); | ||
| 1367 | &menu.shadows | ||
| 1368 | } | ||
| 1263 | }; | 1369 | }; |
| 1264 | self.display.push(Display::Path { | 1370 | for &Shadow { |
| 1265 | data: outline(node.shape, size, node.radius), | 1371 | color, |
| 1266 | origin: [painted[0], painted[1] + drop], | 1372 | blur, |
| 1267 | style: PathStyle::Shadow(spread), | 1373 | drop, |
| 1268 | colors: [shadow; 2], | 1374 | spread, |
| 1269 | }); | 1375 | } in shadows |
| 1376 | { | ||
| 1377 | let grown = size.map(|side| side + 2.0 * spread); | ||
| 1378 | self.display.push(Display::Path { | ||
| 1379 | data: outline(node.shape, grown, node.radius + spread), | ||
| 1380 | origin: [painted[0] - spread, painted[1] + drop - spread], | ||
| 1381 | style: PathStyle::Shadow(blur), | ||
| 1382 | colors: [color; 2], | ||
| 1383 | }); | ||
| 1384 | } | ||
| 1270 | } | 1385 | } |
| 1271 | if node.shape == Shape::Rounded { | 1386 | if node.shape == Shape::Rounded { |
| 1272 | if fill.is_some() || border.is_some() { | 1387 | if fill.is_some() || border.is_some() { |
| ... | @@ -1581,6 +1696,7 @@ impl Built { | ... | @@ -1581,6 +1696,7 @@ impl Built { |
| 1581 | offset: spec.offset, | 1696 | offset: spec.offset, |
| 1582 | cursor: spec.cursor, | 1697 | cursor: spec.cursor, |
| 1583 | anchor: spec.anchor, | 1698 | anchor: spec.anchor, |
| 1699 | open: 1.0, | ||
| 1584 | motion: None, | 1700 | motion: None, |
| 1585 | marks: Vec::new(), | 1701 | marks: Vec::new(), |
| 1586 | computed: [0.0; 2], | 1702 | computed: [0.0; 2], |
crates/ui/src/popup.rs+290-60| ... | @@ -3,32 +3,28 @@ | ... | @@ -3,32 +3,28 @@ |
| 3 | //! frame it is, closing the popup. | 3 | //! frame it is, closing the popup. |
| 4 | 4 | ||
| 5 | use crate::{ | 5 | use crate::{ |
| 6 | Anchor, Axis, Event, Flags, ICON, ICON_GAP, Id, List, Popup, Row, Rows, Spec, Ui, children, | 6 | Anchor, Axis, Event, Flags, ICON, ICON_GAP, Id, List, Menu, Overflow, Popup, Row, Rows, Spec, |
| 7 | fill, fit, list::GUTTER, mix, px, text_field, | 7 | TIP_DELAY, TIP_FADE, TIP_WARM, Tip, Ui, children, fill, fit, list::GUTTER, mix, px, text_field, |
| 8 | }; | 8 | }; |
| 9 | use nucleo_matcher::{ | 9 | use nucleo_matcher::{ |
| 10 | Config, Matcher, Utf32Str, | 10 | Config, Matcher, Utf32Str, |
| 11 | pattern::{AtomKind, CaseMatching, Normalization, Pattern}, | 11 | pattern::{AtomKind, CaseMatching, Normalization, Pattern}, |
| 12 | }; | 12 | }; |
| 13 | use std::cell::RefCell; | 13 | use std::{cell::RefCell, time::Duration}; |
| 14 | use winit::keyboard::{Key, NamedKey}; | 14 | use winit::keyboard::{Key, NamedKey}; |
| 15 | 15 | ||
| 16 | pub(crate) const CHECK: &[&str] = &[include_str!("../assets/check.svg")]; | 16 | pub(crate) const CHECK: &[&str] = &[include_str!("../assets/check.svg")]; |
| 17 | /// Inset of a popup's contents, and its distance from what it drops down from. | 17 | /// Inset of a popup's contents, and its distance from what it drops down from. |
| 18 | const PAD: f32 = 4.0; | 18 | pub(crate) const PAD: f32 = 4.0; |
| 19 | /// A palette's rows and filter field, and a menu's more compact rows. | 19 | /// A palette's rows and filter field, and Snowbound's more compact menu rows. |
| 20 | const ROW: f32 = 26.0; | 20 | const ROW: f32 = 26.0; |
| 21 | pub(crate) const MENU_ROW: f32 = 22.0; | 21 | pub(crate) const MENU_ROW: f32 = 22.0; |
| 22 | /// Inset of a menu row's icon and text. | ||
| 23 | const ROW_PAD: f32 = 8.0; | ||
| 24 | /// How far a menu's icons sit inside its leading edge. | ||
| 25 | pub(crate) const ICON_INSET: f32 = PAD + ROW_PAD; | ||
| 26 | /// Rows a list shows before it scrolls. | 22 | /// Rows a list shows before it scrolls. |
| 27 | const ROWS: f32 = 12.0; | 23 | const ROWS: f32 = 12.0; |
| 28 | /// Height of the rule between groups. | ||
| 29 | const RULE: f32 = 9.0; | ||
| 30 | const NARROWEST: f32 = 140.0; | 24 | const NARROWEST: f32 = 140.0; |
| 31 | const PALETTE: f32 = 560.0; | 25 | const PALETTE: f32 = 560.0; |
| 26 | /// Where a tooltip's description wraps. | ||
| 27 | const TIP_WIDTH: f32 = 280.0; | ||
| 32 | /// A colour grid's cell, around its swatch. | 28 | /// A colour grid's cell, around its swatch. |
| 33 | const CELL: f32 = 22.0; | 29 | const CELL: f32 = 22.0; |
| 34 | /// Seconds a popup takes to ease to the height of its results. | 30 | /// Seconds a popup takes to ease to the height of its results. |
| ... | @@ -83,10 +79,16 @@ pub fn menu( | ... | @@ -83,10 +79,16 @@ pub fn menu( |
| 83 | if !ui.popup_open(id) { | 79 | if !ui.popup_open(id) { |
| 84 | return None; | 80 | return None; |
| 85 | } | 81 | } |
| 82 | let style = ui.theme.menu(); | ||
| 83 | let mut measure = |text| { | ||
| 84 | ui.texts | ||
| 85 | .label(text, style.font_size, false, None, ui.frame) | ||
| 86 | .size[0] | ||
| 87 | }; | ||
| 86 | let [text, shortcut] = items.iter().fold([0.0_f32; 2], |[text, shortcut], item| { | 88 | let [text, shortcut] = items.iter().fold([0.0_f32; 2], |[text, shortcut], item| { |
| 87 | [ | 89 | [ |
| 88 | text.max(ui.measure(item.text)[0]), | 90 | text.max(measure(item.text)), |
| 89 | shortcut.max(ui.measure(item.shortcut)[0]), | 91 | shortcut.max(measure(item.shortcut)), |
| 90 | ] | 92 | ] |
| 91 | }); | 93 | }); |
| 92 | let icons = items.iter().any(|item| item.icon.is_some() || item.checked); | 94 | let icons = items.iter().any(|item| item.icon.is_some() || item.checked); |
| ... | @@ -101,10 +103,10 @@ pub fn menu( | ... | @@ -101,10 +103,10 @@ pub fn menu( |
| 101 | } else { | 103 | } else { |
| 102 | 0.0 | 104 | 0.0 |
| 103 | } | 105 | } |
| 104 | + 2.0 * (PAD + 8.0); | 106 | + 2.0 * (style.pad + style.row_pad); |
| 105 | let least = match anchor { | 107 | let least = match anchor { |
| 106 | Anchor::Below(rect) => rect[2] - rect[0], | 108 | Anchor::Below(rect) => rect[2] - rect[0], |
| 107 | Anchor::Over(rect) => rect[2] - rect[0] + 2.0 * PAD, | 109 | Anchor::Over(rect) => rect[2] - rect[0] + 2.0 * style.pad, |
| 108 | _ => 0.0, | 110 | _ => 0.0, |
| 109 | }; | 111 | }; |
| 110 | choose( | 112 | choose( |
| ... | @@ -114,10 +116,98 @@ pub fn menu( | ... | @@ -114,10 +116,98 @@ pub fn menu( |
| 114 | items, | 116 | items, |
| 115 | filter, | 117 | filter, |
| 116 | width.max(least).max(NARROWEST), | 118 | width.max(least).max(NARROWEST), |
| 117 | MENU_ROW, | 119 | style.row, |
| 118 | ) | 120 | ) |
| 119 | } | 121 | } |
| 120 | 122 | ||
| 123 | /// Shows `title`, with the `keys` that run it and a `description` under it, in a tooltip | ||
| 124 | /// below the box built last while the pointer rests on it or on a box inside it: after a | ||
| 125 | /// delay, or at once while another has just shown. A press or the wheel hides it until the | ||
| 126 | /// pointer leaves. | ||
| 127 | pub fn tooltip(ui: &mut Ui, title: &str, keys: &str, description: Option<&str>) { | ||
| 128 | let Some(&index) = ui.nodes[*ui.stack.last().unwrap()].children.last() else { | ||
| 129 | return; | ||
| 130 | }; | ||
| 131 | // The boxes built since are the box's own. | ||
| 132 | let hovered = ui | ||
| 133 | .hover | ||
| 134 | .is_some_and(|hover| ui.nodes[index..].iter().any(|node| node.id == hover)); | ||
| 135 | if !hovered || ui.active.is_some() { | ||
| 136 | return; | ||
| 137 | } | ||
| 138 | let id = ui.nodes[index].id; | ||
| 139 | let now = ui.now; | ||
| 140 | let showing = ui | ||
| 141 | .tip | ||
| 142 | .is_some_and(|tip| tip.due.is_some_and(|due| due <= now)); | ||
| 143 | let warm = showing | ||
| 144 | || ui | ||
| 145 | .warm | ||
| 146 | .is_some_and(|warm| now.saturating_duration_since(warm).as_secs_f32() < TIP_WARM); | ||
| 147 | let due = match ui.tip { | ||
| 148 | Some(tip) if tip.id == id => tip.due, | ||
| 149 | // Shown at once, already faded in. | ||
| 150 | _ if warm => Some( | ||
| 151 | now.checked_sub(Duration::from_secs_f32(TIP_FADE)) | ||
| 152 | .unwrap_or(now), | ||
| 153 | ), | ||
| 154 | _ => Some(now + Duration::from_secs_f32(TIP_DELAY)), | ||
| 155 | }; | ||
| 156 | ui.tip = Some(Tip { | ||
| 157 | id, | ||
| 158 | frame: ui.frame, | ||
| 159 | due, | ||
| 160 | }); | ||
| 161 | let Some(due) = due else { | ||
| 162 | return; | ||
| 163 | }; | ||
| 164 | if now < due { | ||
| 165 | ui.wake = Some(ui.wake.map_or(due, |wake| wake.min(due))); | ||
| 166 | return; | ||
| 167 | } | ||
| 168 | let title = if keys.is_empty() { | ||
| 169 | title.to_owned() | ||
| 170 | } else { | ||
| 171 | format!("{title} ({keys})") | ||
| 172 | }; | ||
| 173 | let [left, top, right, bottom] = ui.rect(id).unwrap_or_default(); | ||
| 174 | let theme = &ui.theme; | ||
| 175 | let spec = Spec { | ||
| 176 | axis: Axis::Y, | ||
| 177 | fill: Some(theme.popup), | ||
| 178 | border: Some(theme.chip), | ||
| 179 | shadow: Some(theme.shadow), | ||
| 180 | radius: 4.0, | ||
| 181 | pad: [8.0, 5.0], | ||
| 182 | gap: 3.0, | ||
| 183 | anchor: Some(Anchor::Below([left, top, right, bottom + PAD])), | ||
| 184 | ..Spec::default() | ||
| 185 | }; | ||
| 186 | ui.open_as(id.child("tooltip"), spec); | ||
| 187 | ui.leaf( | ||
| 188 | "title", | ||
| 189 | Spec { | ||
| 190 | size: [fit(), fit()], | ||
| 191 | text: Some(&title), | ||
| 192 | bold: true, | ||
| 193 | ..Spec::default() | ||
| 194 | }, | ||
| 195 | ); | ||
| 196 | if let Some(description) = description { | ||
| 197 | let width = ui.measure(description)[0].min(TIP_WIDTH); | ||
| 198 | ui.leaf( | ||
| 199 | "description", | ||
| 200 | Spec { | ||
| 201 | size: [px(width), fit()], | ||
| 202 | text: Some(description), | ||
| 203 | overflow: Overflow::Wrap, | ||
| 204 | ..Spec::default() | ||
| 205 | }, | ||
| 206 | ); | ||
| 207 | } | ||
| 208 | ui.close(); | ||
| 209 | } | ||
| 210 | |||
| 121 | /// Builds popup `id` as a command palette across the top of the window while it is open, | 211 | /// Builds popup `id` as a command palette across the top of the window while it is open, |
| 122 | /// listing `commands` that match what is typed, most relevant first. Returns the index of | 212 | /// listing `commands` that match what is typed, most relevant first. Returns the index of |
| 123 | /// the command chosen. | 213 | /// the command chosen. |
| ... | @@ -157,19 +247,11 @@ fn choose( | ... | @@ -157,19 +247,11 @@ fn choose( |
| 157 | let mut query = std::mem::take(&mut popup.query); | 247 | let mut query = std::mem::take(&mut popup.query); |
| 158 | let mut highlight = popup.highlight; | 248 | let mut highlight = popup.highlight; |
| 159 | let theme = ui.theme.clone(); | 249 | let theme = ui.theme.clone(); |
| 160 | // Over a combo box, the field takes the box's place at once and widens as the popup | 250 | let style = theme.menu(); |
| 161 | // comes in around it. | 251 | // Over a combo box, the field takes the box's place at once, widening with the popup. |
| 162 | let (flags, size) = match anchor { | 252 | let (flags, height) = match anchor { |
| 163 | Anchor::Over(rect) => { | 253 | Anchor::Over(rect) => (Flags::STILL, rect[3] - rect[1]), |
| 164 | let open = ui.opening(id, anchor).unwrap_or(1.0); | 254 | _ => (Flags::default(), ROW), |
| 165 | let from = rect[2] - rect[0]; | ||
| 166 | let to = width - 2.0 * PAD; | ||
| 167 | ( | ||
| 168 | Flags::STILL, | ||
| 169 | [px(from + (to - from) * open), px(rect[3] - rect[1])], | ||
| 170 | ) | ||
| 171 | } | ||
| 172 | _ => (Flags::default(), [fill(), px(ROW)]), | ||
| 173 | }; | 255 | }; |
| 174 | surface(ui, id, anchor, width); | 256 | surface(ui, id, anchor, width); |
| 175 | if let Some(placeholder) = filter { | 257 | if let Some(placeholder) = filter { |
| ... | @@ -181,7 +263,7 @@ fn choose( | ... | @@ -181,7 +263,7 @@ fn choose( |
| 181 | placeholder, | 263 | placeholder, |
| 182 | Spec { | 264 | Spec { |
| 183 | flags, | 265 | flags, |
| 184 | size, | 266 | size: [fill(), px(height)], |
| 185 | fill: Some(theme.base), | 267 | fill: Some(theme.base), |
| 186 | border: Some(theme.accent), | 268 | border: Some(theme.accent), |
| 187 | radius: 4.0, | 269 | radius: 4.0, |
| ... | @@ -193,7 +275,7 @@ fn choose( | ... | @@ -193,7 +275,7 @@ fn choose( |
| 193 | highlight = None; | 275 | highlight = None; |
| 194 | } | 276 | } |
| 195 | } | 277 | } |
| 196 | let matches = Matches::new(items, &query); | 278 | let matches = Matches::new(items, &query, style.rule_band); |
| 197 | // Unfiltered, the current or checked item starts highlighted; filtered, the best match. | 279 | // Unfiltered, the current or checked item starts highlighted; filtered, the best match. |
| 198 | highlight = highlight.or_else(|| { | 280 | highlight = highlight.or_else(|| { |
| 199 | let first = if query.is_empty() { | 281 | let first = if query.is_empty() { |
| ... | @@ -207,7 +289,11 @@ fn choose( | ... | @@ -207,7 +289,11 @@ fn choose( |
| 207 | first.map(|row| matches.key(row)) | 289 | first.map(|row| matches.key(row)) |
| 208 | }); | 290 | }); |
| 209 | let window = ui.rect(Id::ROOT).map_or(0.0, |window| window[3]); | 291 | let window = ui.rect(Id::ROOT).map_or(0.0, |window| window[3]); |
| 210 | let field = if filter.is_some() { ROW + PAD } else { 0.0 }; | 292 | let field = if filter.is_some() { |
| 293 | ROW + style.pad | ||
| 294 | } else { | ||
| 295 | 0.0 | ||
| 296 | }; | ||
| 211 | let content = matches.count() as f32 * row + matches.space_before(matches.count()); | 297 | let content = matches.count() as f32 * row + matches.space_before(matches.count()); |
| 212 | let view = content | 298 | let view = content |
| 213 | .max(row) | 299 | .max(row) |
| ... | @@ -236,7 +322,8 @@ fn choose( | ... | @@ -236,7 +322,8 @@ fn choose( |
| 236 | Spec { | 322 | Spec { |
| 237 | size: [fill(), px(row)], | 323 | size: [fill(), px(row)], |
| 238 | text: Some("No matches"), | 324 | text: Some("No matches"), |
| 239 | color: Some(theme.text_dim), | 325 | font_size: Some(style.font_size), |
| 326 | color: Some(style.dim), | ||
| 240 | pad: [8.0, 0.0], | 327 | pad: [8.0, 0.0], |
| 241 | ..Spec::default() | 328 | ..Spec::default() |
| 242 | }, | 329 | }, |
| ... | @@ -254,12 +341,12 @@ fn choose( | ... | @@ -254,12 +341,12 @@ fn choose( |
| 254 | id.child("rows"), | 341 | id.child("rows"), |
| 255 | Spec { | 342 | Spec { |
| 256 | size: [fill(), px(view)], | 343 | size: [fill(), px(view)], |
| 257 | fill: Some(theme.popup), | 344 | fill: Some(style.fill), |
| 258 | ..Spec::default() | 345 | ..Spec::default() |
| 259 | }, | 346 | }, |
| 260 | list, | 347 | list, |
| 261 | &mut highlight, | 348 | &mut highlight, |
| 262 | |ui, row| menu_row(ui, &theme, &matches, row), | 349 | |ui, row| menu_row(ui, &style, &matches, row), |
| 263 | ); | 350 | ); |
| 264 | let entered = keys.contains(&NamedKey::Enter); | 351 | let entered = keys.contains(&NamedKey::Enter); |
| 265 | clicked.or_else(|| { | 352 | clicked.or_else(|| { |
| ... | @@ -282,7 +369,7 @@ fn choose( | ... | @@ -282,7 +369,7 @@ fn choose( |
| 282 | 369 | ||
| 283 | /// Builds a menu's row: the item's icon or check, its text and its shortcut, on the | 370 | /// Builds a menu's row: the item's icon or check, its text and its shortcut, on the |
| 284 | /// highlight when selected, under a rule when it starts a group. | 371 | /// highlight when selected, under a rule when it starts a group. |
| 285 | fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { | 372 | fn menu_row(ui: &mut Ui, style: &Menu, matches: &Matches, row: Row) { |
| 286 | let item = &matches.items[row.key as usize]; | 373 | let item = &matches.items[row.key as usize]; |
| 287 | if row.index > 0 && matches.ruled && item.separated { | 374 | if row.index > 0 && matches.ruled && item.separated { |
| 288 | ui.leaf( | 375 | ui.leaf( |
| ... | @@ -290,24 +377,28 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { | ... | @@ -290,24 +377,28 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { |
| 290 | Spec { | 377 | Spec { |
| 291 | flags: Flags::FLOAT, | 378 | flags: Flags::FLOAT, |
| 292 | size: [fill(), px(1.0)], | 379 | size: [fill(), px(1.0)], |
| 293 | position: [0.0, -(RULE + 1.0) / 2.0], | 380 | position: [0.0, -(style.rule_band + 1.0) / 2.0], |
| 294 | fill: Some(theme.chip), | 381 | inset: [style.rule_inset, 0.0, style.rule_inset, 0.0], |
| 382 | fill: Some(style.rule), | ||
| 295 | ..Spec::default() | 383 | ..Spec::default() |
| 296 | }, | 384 | }, |
| 297 | ); | 385 | ); |
| 298 | } | 386 | } |
| 299 | let color = if item.disabled || item.heading { | 387 | let color = if item.heading { |
| 300 | theme.text_dim | 388 | style.dim |
| 389 | } else if item.disabled { | ||
| 390 | style.disabled | ||
| 301 | } else { | 391 | } else { |
| 302 | theme.text | 392 | style.text |
| 303 | }; | 393 | }; |
| 304 | ui.open( | 394 | ui.open( |
| 305 | "item", | 395 | "item", |
| 306 | Spec { | 396 | Spec { |
| 307 | size: [fill(), fill()], | 397 | size: [fill(), fill()], |
| 308 | fill: row.selected.then(|| theme.hover()), | 398 | fill: row.selected.then_some(style.highlight), |
| 309 | radius: 4.0, | 399 | border: style.highlight_border.filter(|_| row.selected), |
| 310 | pad: [ROW_PAD, 0.0], | 400 | radius: style.row_radius, |
| 401 | pad: [style.row_pad, 0.0], | ||
| 311 | gap: ICON_GAP, | 402 | gap: ICON_GAP, |
| 312 | ..Spec::default() | 403 | ..Spec::default() |
| 313 | }, | 404 | }, |
| ... | @@ -324,7 +415,7 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { | ... | @@ -324,7 +415,7 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { |
| 324 | size: [px(ICON), fill()], | 415 | size: [px(ICON), fill()], |
| 325 | icon: if item.checked { Some(CHECK) } else { item.icon }, | 416 | icon: if item.checked { Some(CHECK) } else { item.icon }, |
| 326 | color: Some(if item.disabled { | 417 | color: Some(if item.disabled { |
| 327 | mix(tint, theme.popup, 0.5) | 418 | mix(tint, style.fill, 0.5) |
| 328 | } else { | 419 | } else { |
| 329 | tint | 420 | tint |
| 330 | }), | 421 | }), |
| ... | @@ -338,7 +429,7 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { | ... | @@ -338,7 +429,7 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { |
| 338 | size: [fill(), fill()], | 429 | size: [fill(), fill()], |
| 339 | text: Some(item.text), | 430 | text: Some(item.text), |
| 340 | font: item.font, | 431 | font: item.font, |
| 341 | font_size: item.heading.then_some(theme.font_size - 2.0), | 432 | font_size: Some(style.font_size - if item.heading { 2.0 } else { 0.0 }), |
| 342 | bold: item.heading, | 433 | bold: item.heading, |
| 343 | color: Some(color), | 434 | color: Some(color), |
| 344 | ..Spec::default() | 435 | ..Spec::default() |
| ... | @@ -350,7 +441,8 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { | ... | @@ -350,7 +441,8 @@ fn menu_row(ui: &mut Ui, theme: &crate::Theme, matches: &Matches, row: Row) { |
| 350 | Spec { | 441 | Spec { |
| 351 | size: [fit(), fill()], | 442 | size: [fit(), fill()], |
| 352 | text: Some(item.shortcut), | 443 | text: Some(item.shortcut), |
| 353 | color: Some(theme.text_dim), | 444 | font_size: Some(style.font_size), |
| 445 | color: Some(style.dim), | ||
| 354 | ..Spec::default() | 446 | ..Spec::default() |
| 355 | }, | 447 | }, |
| 356 | ); | 448 | ); |
| ... | @@ -419,7 +511,12 @@ pub fn colors( | ... | @@ -419,7 +511,12 @@ pub fn colors( |
| 419 | 511 | ||
| 420 | let theme = ui.theme.clone(); | 512 | let theme = ui.theme.clone(); |
| 421 | let lit = |index: usize| (highlight == Some(index)).then(|| theme.hover()); | 513 | let lit = |index: usize| (highlight == Some(index)).then(|| theme.hover()); |
| 422 | surface(ui, id, anchor, columns as f32 * CELL + 2.0 * PAD); | 514 | surface( |
| 515 | ui, | ||
| 516 | id, | ||
| 517 | anchor, | ||
| 518 | columns as f32 * CELL + 2.0 * ui.theme.menu().pad, | ||
| 519 | ); | ||
| 423 | ui.open_as( | 520 | ui.open_as( |
| 424 | cell(0), | 521 | cell(0), |
| 425 | Spec { | 522 | Spec { |
| ... | @@ -473,6 +570,130 @@ pub fn colors( | ... | @@ -473,6 +570,130 @@ pub fn colors( |
| 473 | None | 570 | None |
| 474 | } | 571 | } |
| 475 | 572 | ||
| 573 | /// Builds popup `id` as a grid of `size` columns and rows beside `anchor` while it is open, | ||
| 574 | /// lit from its corner to the cell pointed at, as Office's table picker is. Returns the | ||
| 575 | /// columns and rows chosen. | ||
| 576 | pub fn table_picker(ui: &mut Ui, id: Id, anchor: Anchor, size: [usize; 2]) -> Option<[usize; 2]> { | ||
| 577 | if !ui.popup_open(id) { | ||
| 578 | return None; | ||
| 579 | } | ||
| 580 | let cell = |index: usize| id.child(("cell", index)); | ||
| 581 | let keys = navigation( | ||
| 582 | ui, | ||
| 583 | &[id], | ||
| 584 | &[ | ||
| 585 | NamedKey::ArrowLeft, | ||
| 586 | NamedKey::ArrowRight, | ||
| 587 | NamedKey::ArrowUp, | ||
| 588 | NamedKey::ArrowDown, | ||
| 589 | NamedKey::Enter, | ||
| 590 | ], | ||
| 591 | ); | ||
| 592 | let [columns, rows] = size; | ||
| 593 | let mut highlight = state(ui, id).highlight.map(|cell| cell as usize); | ||
| 594 | let mut chosen = None; | ||
| 595 | for index in 0..columns * rows { | ||
| 596 | let signal = ui.signal(cell(index)); | ||
| 597 | if signal.hovered && ui.moved { | ||
| 598 | highlight = Some(index); | ||
| 599 | } | ||
| 600 | if signal.clicked { | ||
| 601 | chosen = Some(index); | ||
| 602 | } | ||
| 603 | } | ||
| 604 | for key in keys { | ||
| 605 | let at = highlight.unwrap_or(0); | ||
| 606 | let [column, row] = [at % columns, at / columns]; | ||
| 607 | highlight = Some(match key { | ||
| 608 | NamedKey::Enter => { | ||
| 609 | chosen = chosen.or(highlight); | ||
| 610 | continue; | ||
| 611 | } | ||
| 612 | _ if highlight.is_none() => 0, | ||
| 613 | NamedKey::ArrowLeft => at - usize::from(column > 0), | ||
| 614 | NamedKey::ArrowRight => at + usize::from(column + 1 < columns), | ||
| 615 | NamedKey::ArrowUp => at - if row > 0 { columns } else { 0 }, | ||
| 616 | _ => at + if row + 1 < rows { columns } else { 0 }, | ||
| 617 | }); | ||
| 618 | } | ||
| 619 | let extent = |index: usize| [index % columns + 1, index / columns + 1]; | ||
| 620 | if let Some(index) = chosen { | ||
| 621 | ui.close_popup(id); | ||
| 622 | return Some(extent(index)); | ||
| 623 | } | ||
| 624 | let theme = ui.theme.clone(); | ||
| 625 | let reach = highlight.map_or([0, 0], extent); | ||
| 626 | surface( | ||
| 627 | ui, | ||
| 628 | id, | ||
| 629 | anchor, | ||
| 630 | columns as f32 * CELL + 2.0 * ui.theme.menu().pad, | ||
| 631 | ); | ||
| 632 | let heading = match highlight { | ||
| 633 | Some(_) => format!("{}x{} Table", reach[0], reach[1]), | ||
| 634 | None => "Insert Table".to_owned(), | ||
| 635 | }; | ||
| 636 | ui.leaf( | ||
| 637 | "heading", | ||
| 638 | Spec { | ||
| 639 | size: [fill(), px(ROW)], | ||
| 640 | text: Some(&heading), | ||
| 641 | bold: true, | ||
| 642 | pad: [8.0, 0.0], | ||
| 643 | ..Spec::default() | ||
| 644 | }, | ||
| 645 | ); | ||
| 646 | ui.open( | ||
| 647 | "grid", | ||
| 648 | Spec { | ||
| 649 | axis: Axis::Y, | ||
| 650 | size: [fill(), children()], | ||
| 651 | ..Spec::default() | ||
| 652 | }, | ||
| 653 | ); | ||
| 654 | for row in 0..rows { | ||
| 655 | ui.open( | ||
| 656 | ("row", row), | ||
| 657 | Spec { | ||
| 658 | size: [fill(), px(CELL)], | ||
| 659 | ..Spec::default() | ||
| 660 | }, | ||
| 661 | ); | ||
| 662 | for column in 0..columns { | ||
| 663 | let lit = column < reach[0] && row < reach[1]; | ||
| 664 | ui.open_as( | ||
| 665 | cell(row * columns + column), | ||
| 666 | Spec { | ||
| 667 | flags: Flags::CLICKABLE, | ||
| 668 | size: [px(CELL), px(CELL)], | ||
| 669 | pad: [3.0, 3.0], | ||
| 670 | ..Spec::default() | ||
| 671 | }, | ||
| 672 | ); | ||
| 673 | ui.leaf( | ||
| 674 | "square", | ||
| 675 | Spec { | ||
| 676 | size: [fill(), fill()], | ||
| 677 | fill: Some(if lit { theme.hover() } else { theme.base }), | ||
| 678 | border: Some(if lit { | ||
| 679 | theme.accent | ||
| 680 | } else { | ||
| 681 | mix(theme.text, theme.popup, 0.7) | ||
| 682 | }), | ||
| 683 | radius: 2.0, | ||
| 684 | ..Spec::default() | ||
| 685 | }, | ||
| 686 | ); | ||
| 687 | ui.close(); | ||
| 688 | } | ||
| 689 | ui.close(); | ||
| 690 | } | ||
| 691 | ui.close(); | ||
| 692 | ui.close(); | ||
| 693 | state(ui, id).highlight = highlight.map(|cell| cell as u64); | ||
| 694 | None | ||
| 695 | } | ||
| 696 | |||
| 476 | /// Builds popup `id` as a gallery beside `anchor` while it is open: `groups` of cells, each | 697 | /// Builds popup `id` as a gallery beside `anchor` while it is open: `groups` of cells, each |
| 477 | /// a heading and a count, in rows of `columns` cells `size` large, with cell `current` | 698 | /// a heading and a count, in rows of `columns` cells `size` large, with cell `current` |
| 478 | /// outlined. `cell` builds a cell's contents from its index, counted through the groups. | 699 | /// outlined. `cell` builds a cell's contents from its index, counted through the groups. |
| ... | @@ -533,7 +754,12 @@ pub fn gallery( | ... | @@ -533,7 +754,12 @@ pub fn gallery( |
| 533 | return Some(index); | 754 | return Some(index); |
| 534 | } | 755 | } |
| 535 | let theme = ui.theme.clone(); | 756 | let theme = ui.theme.clone(); |
| 536 | surface(ui, id, anchor, columns as f32 * size[0] + 2.0 * PAD); | 757 | surface( |
| 758 | ui, | ||
| 759 | id, | ||
| 760 | anchor, | ||
| 761 | columns as f32 * size[0] + 2.0 * ui.theme.menu().pad, | ||
| 762 | ); | ||
| 537 | let mut index = 0; | 763 | let mut index = 0; |
| 538 | for (group, (heading, cells)) in groups.iter().enumerate() { | 764 | for (group, (heading, cells)) in groups.iter().enumerate() { |
| 539 | ui.leaf( | 765 | ui.leaf( |
| ... | @@ -584,29 +810,30 @@ pub fn gallery( | ... | @@ -584,29 +810,30 @@ pub fn gallery( |
| 584 | 810 | ||
| 585 | /// Opens popup `id`'s panel `width` wide beside `anchor`; the caller closes it. | 811 | /// Opens popup `id`'s panel `width` wide beside `anchor`; the caller closes it. |
| 586 | fn surface(ui: &mut Ui, id: Id, anchor: Anchor, width: f32) { | 812 | fn surface(ui: &mut Ui, id: Id, anchor: Anchor, width: f32) { |
| 813 | let style = ui.theme.menu(); | ||
| 814 | let pad = style.pad; | ||
| 587 | let anchor = match anchor { | 815 | let anchor = match anchor { |
| 588 | Anchor::Below([left, top, right, bottom]) => { | 816 | Anchor::Below([left, top, right, bottom]) => { |
| 589 | Anchor::Below([left, top - PAD, right, bottom + PAD]) | 817 | Anchor::Below([left, top - PAD, right, bottom + PAD]) |
| 590 | } | 818 | } |
| 591 | // Level with the row it opens from, or with its contents over the box. | 819 | // Level with the row it opens from, or with its contents over the box. |
| 592 | Anchor::Right([left, top, right, bottom]) => { | 820 | Anchor::Right([left, top, right, bottom]) => { |
| 593 | Anchor::Right([left, top - PAD, right, bottom + PAD]) | 821 | Anchor::Right([left, top - pad, right, bottom + pad]) |
| 594 | } | 822 | } |
| 595 | Anchor::Over([left, top, right, bottom]) => { | 823 | Anchor::Over([left, top, right, bottom]) => { |
| 596 | Anchor::Over([left - PAD, top - PAD, right + PAD, bottom + PAD]) | 824 | Anchor::Over([left - pad, top - pad, right + pad, bottom + pad]) |
| 597 | } | 825 | } |
| 598 | point => point, | 826 | point => point, |
| 599 | }; | 827 | }; |
| 600 | let theme = &ui.theme; | ||
| 601 | let spec = Spec { | 828 | let spec = Spec { |
| 602 | axis: Axis::Y, | 829 | axis: Axis::Y, |
| 603 | size: [px(width), children()], | 830 | size: [px(width), children()], |
| 604 | fill: Some(theme.popup), | 831 | fill: Some(style.fill), |
| 605 | border: Some(theme.chip), | 832 | border: Some(style.border), |
| 606 | shadow: Some(theme.shadow), | 833 | shadow: Some(ui.theme.shadow), |
| 607 | radius: 6.0, | 834 | radius: style.radius, |
| 608 | pad: [PAD; 2], | 835 | pad: [pad; 2], |
| 609 | gap: PAD, | 836 | gap: pad, |
| 610 | anchor: Some(anchor), | 837 | anchor: Some(anchor), |
| 611 | ..Spec::default() | 838 | ..Spec::default() |
| 612 | }; | 839 | }; |
| ... | @@ -661,11 +888,13 @@ pub(crate) struct Matches<'a> { | ... | @@ -661,11 +888,13 @@ pub(crate) struct Matches<'a> { |
| 661 | /// Rules above each row and the end, counted from the top; only while unfiltered. | 888 | /// Rules above each row and the end, counted from the top; only while unfiltered. |
| 662 | rules: Vec<u32>, | 889 | rules: Vec<u32>, |
| 663 | ruled: bool, | 890 | ruled: bool, |
| 891 | /// Height of the band a rule takes. | ||
| 892 | rule: f32, | ||
| 664 | icons: bool, | 893 | icons: bool, |
| 665 | } | 894 | } |
| 666 | 895 | ||
| 667 | impl<'a> Matches<'a> { | 896 | impl<'a> Matches<'a> { |
| 668 | pub(crate) fn new(items: &'a [Item<'a>], query: &str) -> Self { | 897 | pub(crate) fn new(items: &'a [Item<'a>], query: &str, rule: f32) -> Self { |
| 669 | let pattern = Pattern::new( | 898 | let pattern = Pattern::new( |
| 670 | query, | 899 | query, |
| 671 | CaseMatching::Ignore, | 900 | CaseMatching::Ignore, |
| ... | @@ -708,6 +937,7 @@ impl<'a> Matches<'a> { | ... | @@ -708,6 +937,7 @@ impl<'a> Matches<'a> { |
| 708 | rows, | 937 | rows, |
| 709 | rules, | 938 | rules, |
| 710 | ruled, | 939 | ruled, |
| 940 | rule, | ||
| 711 | icons: items.iter().any(|item| item.icon.is_some() || item.checked), | 941 | icons: items.iter().any(|item| item.icon.is_some() || item.checked), |
| 712 | } | 942 | } |
| 713 | } | 943 | } |
| ... | @@ -732,6 +962,6 @@ impl Rows for Matches<'_> { | ... | @@ -732,6 +962,6 @@ impl Rows for Matches<'_> { |
| 732 | } | 962 | } |
| 733 | 963 | ||
| 734 | fn space_before(&self, index: usize) -> f32 { | 964 | fn space_before(&self, index: usize) -> f32 { |
| 735 | self.rules[index] as f32 * RULE | 965 | self.rules[index] as f32 * self.rule |
| 736 | } | 966 | } |
| 737 | } | 967 | } |
crates/ui/src/shell.rs+46-1| ... | @@ -222,6 +222,49 @@ pub fn tool_button( | ... | @@ -222,6 +222,49 @@ pub fn tool_button( |
| 222 | ) | 222 | ) |
| 223 | } | 223 | } |
| 224 | 224 | ||
| 225 | /// A tool button, with a menu arrow where `menu`, whose command does not apply now: `icon` | ||
| 226 | /// tinted by `tint` shows faded, and it takes no clicks. | ||
| 227 | pub fn unavailable( | ||
| 228 | ui: &mut Ui, | ||
| 229 | part: impl Hash, | ||
| 230 | icon: &'static [&'static str], | ||
| 231 | tint: [f32; 4], | ||
| 232 | menu: bool, | ||
| 233 | ) { | ||
| 234 | let faded = |[red, green, blue, alpha]: [f32; 4]| [red, green, blue, alpha * 0.35]; | ||
| 235 | let arrow = faded(ui.theme.text_dim); | ||
| 236 | ui.open( | ||
| 237 | part, | ||
| 238 | Spec { | ||
| 239 | size: [children(), px(TOOL)], | ||
| 240 | ..Spec::default() | ||
| 241 | }, | ||
| 242 | ); | ||
| 243 | ui.leaf( | ||
| 244 | "icon", | ||
| 245 | Spec { | ||
| 246 | size: [px(TOOL), px(TOOL)], | ||
| 247 | icon: Some(icon), | ||
| 248 | color: Some(faded(tint)), | ||
| 249 | center: true, | ||
| 250 | ..Spec::default() | ||
| 251 | }, | ||
| 252 | ); | ||
| 253 | if menu { | ||
| 254 | ui.leaf( | ||
| 255 | "arrow", | ||
| 256 | Spec { | ||
| 257 | size: [px(ARROW), px(TOOL)], | ||
| 258 | icon: Some(CHEVRON), | ||
| 259 | color: Some(arrow), | ||
| 260 | center: true, | ||
| 261 | ..Spec::default() | ||
| 262 | }, | ||
| 263 | ); | ||
| 264 | } | ||
| 265 | ui.close(); | ||
| 266 | } | ||
| 267 | |||
| 225 | /// A button showing `icon`, lit while `on`, joined to an arrow that opens popup `menu`, | 268 | /// A button showing `icon`, lit while `on`, joined to an arrow that opens popup `menu`, |
| 226 | /// with the colour it applies as a bar under the icon when given. Hovering the button fills | 269 | /// with the colour it applies as a bar under the icon when given. Hovering the button fills |
| 227 | /// it alone; hovering the arrow, or its menu being open, outlines both as one control. | 270 | /// it alone; hovering the arrow, or its menu being open, outlines both as one control. |
| ... | @@ -359,7 +402,9 @@ pub fn menu_button( | ... | @@ -359,7 +402,9 @@ pub fn menu_button( |
| 359 | ui.open_popup(menu); | 402 | ui.open_popup(menu); |
| 360 | } | 403 | } |
| 361 | let [left, top, right, bottom] = ui.rect(id).unwrap_or_default(); | 404 | let [left, top, right, bottom] = ui.rect(id).unwrap_or_default(); |
| 362 | let shift = crate::popup::ICON_INSET - (TOOL - crate::ICON) / 2.0; | 405 | let menu = ui.theme.menu(); |
| 406 | // The menu's icons line up under the button's. | ||
| 407 | let shift = menu.pad + menu.row_pad - (TOOL - crate::ICON) / 2.0; | ||
| 363 | Anchor::Below([left - shift, top, right - shift, bottom]) | 408 | Anchor::Below([left - shift, top, right - shift, bottom]) |
| 364 | } | 409 | } |
| 365 | 410 |
crates/ui/src/tests.rs+183-1| ... | @@ -777,6 +777,74 @@ fn click(ui: &mut Ui, point: [f32; 2]) { | ... | @@ -777,6 +777,74 @@ fn click(ui: &mut Ui, point: [f32; 2]) { |
| 777 | press(ui, at, false); | 777 | press(ui, at, false); |
| 778 | } | 778 | } |
| 779 | 779 | ||
| 780 | /// Two tool buttons with tooltips, the second at the window's trailing edge; returns | ||
| 781 | /// their tooltips' rectangles. | ||
| 782 | fn tip_frame(ui: &mut Ui) -> [Option<[f32; 4]>; 2] { | ||
| 783 | let mut tips = [Id::ROOT; 2]; | ||
| 784 | frame(ui, |ui| { | ||
| 785 | for (index, x) in [0.0, 380.0].into_iter().enumerate() { | ||
| 786 | let id = ui.open( | ||
| 787 | ("tool", index), | ||
| 788 | Spec { | ||
| 789 | flags: Flags::CLICKABLE | Flags::FLOAT, | ||
| 790 | size: [px(20.0), px(20.0)], | ||
| 791 | position: [x, 0.0], | ||
| 792 | ..Spec::default() | ||
| 793 | }, | ||
| 794 | ); | ||
| 795 | ui.close(); | ||
| 796 | popup::tooltip(ui, "Bold", "Ctrl+B", Some("Makes the selected text bold.")); | ||
| 797 | tips[index] = id.child("tooltip"); | ||
| 798 | } | ||
| 799 | }); | ||
| 800 | tips.map(|tip| ui.rect(tip)) | ||
| 801 | } | ||
| 802 | |||
| 803 | /// Frames the pointer rests through for `millis`, returning the tooltips as they end. | ||
| 804 | fn rest(ui: &mut Ui, millis: u32) -> [Option<[f32; 4]>; 2] { | ||
| 805 | let mut tips = [None; 2]; | ||
| 806 | for _ in 0..millis / 16 { | ||
| 807 | tips = tip_frame(ui); | ||
| 808 | } | ||
| 809 | tips | ||
| 810 | } | ||
| 811 | |||
| 812 | #[test] | ||
| 813 | fn tooltips_wait_then_switch_at_once_and_hide_on_press_until_left() { | ||
| 814 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | ||
| 815 | tip_frame(&mut ui); | ||
| 816 | ui.event(Event::PointerMoved([10.0, 10.0])); | ||
| 817 | assert_eq!(rest(&mut ui, 400), [None; 2]); | ||
| 818 | assert!(ui.wake_at().is_some(), "the delay wakes the host"); | ||
| 819 | let [Some(first), None] = rest(&mut ui, 200) else { | ||
| 820 | panic!("the first tooltip shows after its delay"); | ||
| 821 | }; | ||
| 822 | assert_eq!(first[..2], [0.0, 20.0 + popup::PAD]); | ||
| 823 | |||
| 824 | ui.event(Event::PointerMoved([390.0, 10.0])); | ||
| 825 | let [None, Some(second)] = tip_frame(&mut ui) else { | ||
| 826 | panic!("moving to another shows its tooltip at once"); | ||
| 827 | }; | ||
| 828 | assert_eq!(second[2], 400.0, "kept inside the window"); | ||
| 829 | |||
| 830 | press(&mut ui, Instant::now(), true); | ||
| 831 | press(&mut ui, Instant::now(), false); | ||
| 832 | assert_eq!( | ||
| 833 | rest(&mut ui, 1000), | ||
| 834 | [None; 2], | ||
| 835 | "a press hides it while hovered" | ||
| 836 | ); | ||
| 837 | ui.event(Event::PointerMoved([200.0, 10.0])); | ||
| 838 | rest(&mut ui, 100); | ||
| 839 | ui.event(Event::PointerMoved([10.0, 10.0])); | ||
| 840 | assert_eq!( | ||
| 841 | rest(&mut ui, 100), | ||
| 842 | [None; 2], | ||
| 843 | "after a press it waits again" | ||
| 844 | ); | ||
| 845 | assert!(rest(&mut ui, 500)[0].is_some()); | ||
| 846 | } | ||
| 847 | |||
| 780 | #[test] | 848 | #[test] |
| 781 | fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() { | 849 | fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() { |
| 782 | let mut ui = open_menu(None); | 850 | let mut ui = open_menu(None); |
| ... | @@ -1062,6 +1130,40 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() { | ... | @@ -1062,6 +1130,40 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() { |
| 1062 | assert_eq!(build(&mut ui), Some(None)); | 1130 | assert_eq!(build(&mut ui), Some(None)); |
| 1063 | } | 1131 | } |
| 1064 | 1132 | ||
| 1133 | #[test] | ||
| 1134 | fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() { | ||
| 1135 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | ||
| 1136 | let grid = Id::ROOT.child("table"); | ||
| 1137 | let build = |ui: &mut Ui| { | ||
| 1138 | let mut chosen = None; | ||
| 1139 | frame(ui, |ui| { | ||
| 1140 | chosen = popup::table_picker(ui, grid, BELOW, [4, 3]); | ||
| 1141 | }); | ||
| 1142 | chosen | ||
| 1143 | }; | ||
| 1144 | build(&mut ui); | ||
| 1145 | ui.open_popup(grid); | ||
| 1146 | build(&mut ui); | ||
| 1147 | for named in [ | ||
| 1148 | NamedKey::ArrowDown, | ||
| 1149 | NamedKey::ArrowRight, | ||
| 1150 | NamedKey::ArrowRight, | ||
| 1151 | NamedKey::ArrowDown, | ||
| 1152 | NamedKey::ArrowDown, | ||
| 1153 | NamedKey::Enter, | ||
| 1154 | ] { | ||
| 1155 | ui.event(key(named)); | ||
| 1156 | } | ||
| 1157 | assert_eq!(build(&mut ui), Some([3, 3])); | ||
| 1158 | ui.open_popup(grid); | ||
| 1159 | for _ in 0..40 { | ||
| 1160 | build(&mut ui); | ||
| 1161 | } | ||
| 1162 | let rect = ui.rect(grid.child(("cell", 5_usize))).unwrap(); | ||
| 1163 | click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]); | ||
| 1164 | assert_eq!(build(&mut ui), Some([2, 2])); | ||
| 1165 | } | ||
| 1166 | |||
| 1065 | #[test] | 1167 | #[test] |
| 1066 | fn popups_opened_within_a_popup_stay_above_it() { | 1168 | fn popups_opened_within_a_popup_stay_above_it() { |
| 1067 | let mut ui = open_menu(None); | 1169 | let mut ui = open_menu(None); |
| ... | @@ -1369,7 +1471,7 @@ fn fonts() -> Vec<popup::Item<'static>> { | ... | @@ -1369,7 +1471,7 @@ fn fonts() -> Vec<popup::Item<'static>> { |
| 1369 | } | 1471 | } |
| 1370 | 1472 | ||
| 1371 | fn ranked(items: &[popup::Item], query: &str) -> Vec<&'static str> { | 1473 | fn ranked(items: &[popup::Item], query: &str) -> Vec<&'static str> { |
| 1372 | let order = popup::Matches::new(items, query).order; | 1474 | let order = popup::Matches::new(items, query, 9.0).order; |
| 1373 | order.into_iter().map(|index| FONTS[index]).collect() | 1475 | order.into_iter().map(|index| FONTS[index]).collect() |
| 1374 | } | 1476 | } |
| 1375 | 1477 | ||
| ... | @@ -1836,6 +1938,39 @@ fn a_combo_s_field_holds_its_place_as_its_list_opens_and_its_rows_take_clicks() | ... | @@ -1836,6 +1938,39 @@ fn a_combo_s_field_holds_its_place_as_its_list_opens_and_its_rows_take_clicks() |
| 1836 | assert_eq!(build(&mut ui), Some(3)); | 1938 | assert_eq!(build(&mut ui), Some(3)); |
| 1837 | } | 1939 | } |
| 1838 | 1940 | ||
| 1941 | #[test] | ||
| 1942 | fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() { | ||
| 1943 | let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK); | ||
| 1944 | let fonts = fonts(); | ||
| 1945 | let anchor = Anchor::Over([330.0, 10.0, 390.0, 30.0]); | ||
| 1946 | let build = |ui: &mut Ui| { | ||
| 1947 | frame(ui, |ui| { | ||
| 1948 | _ = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font")) | ||
| 1949 | }) | ||
| 1950 | }; | ||
| 1951 | build(&mut ui); | ||
| 1952 | ui.open_popup(menu_id()); | ||
| 1953 | build(&mut ui); | ||
| 1954 | let field = menu_id().child("filter"); | ||
| 1955 | let edges = | ||
| 1956 | |ui: &Ui| [ui.rect(field).unwrap(), ui.rect(menu_id()).unwrap()].map(|rect| rect[2]); | ||
| 1957 | let first = ui.rect(field).unwrap(); | ||
| 1958 | assert_eq!( | ||
| 1959 | edges(&ui), | ||
| 1960 | [390.0, 394.0], | ||
| 1961 | "the trailing edges start on the box" | ||
| 1962 | ); | ||
| 1963 | assert!(first[0] > 290.0, "the field starts near the box"); | ||
| 1964 | for _ in 0..20 { | ||
| 1965 | build(&mut ui); | ||
| 1966 | } | ||
| 1967 | assert_eq!(edges(&ui), [390.0, 394.0], "the trailing edges hold"); | ||
| 1968 | assert!( | ||
| 1969 | ui.rect(field).unwrap()[0] < first[0] - 30.0, | ||
| 1970 | "the field widens leftward" | ||
| 1971 | ); | ||
| 1972 | } | ||
| 1973 | |||
| 1839 | fn built(ui: &Ui, id: Id) -> &Built { | 1974 | fn built(ui: &Ui, id: Id) -> &Built { |
| 1840 | ui.nodes.iter().find(|node| node.id == id).unwrap() | 1975 | ui.nodes.iter().find(|node| node.id == id).unwrap() |
| 1841 | } | 1976 | } |
| ... | @@ -2024,3 +2159,50 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() { | ... | @@ -2024,3 +2159,50 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() { |
| 2024 | click(&mut ui, point); | 2159 | click(&mut ui, point); |
| 2025 | assert_eq!(build(&mut ui), Some(1)); | 2160 | assert_eq!(build(&mut ui), Some(1)); |
| 2026 | } | 2161 | } |
| 2162 | |||
| 2163 | #[test] | ||
| 2164 | fn desktop_menus_cut_or_fade_without_growing() { | ||
| 2165 | let motion = |ui: &Ui| { | ||
| 2166 | ui.layers().iter().find_map(|layer| match layer { | ||
| 2167 | Layer::Primitives(primitives) => primitives.motion, | ||
| 2168 | Layer::Custom { .. } => None, | ||
| 2169 | }) | ||
| 2170 | }; | ||
| 2171 | let at = Anchor::Point([50.0, 50.0]); | ||
| 2172 | let styled = |motion| { | ||
| 2173 | let mut theme = Theme::light(); | ||
| 2174 | theme.desktop_menu = Some(Menu { | ||
| 2175 | motion, | ||
| 2176 | ..theme.menu() | ||
| 2177 | }); | ||
| 2178 | let mut ui = Ui::new(theme, DOUBLE_CLICK); | ||
| 2179 | menu_frame(&mut ui, at, None); | ||
| 2180 | ui.open_popup(menu_id()); | ||
| 2181 | menu_frame(&mut ui, at, None); | ||
| 2182 | ui | ||
| 2183 | }; | ||
| 2184 | |||
| 2185 | let mut ui = styled(PopupMotion::Cut); | ||
| 2186 | let shown = motion(&ui).unwrap(); | ||
| 2187 | assert_eq!([shown.opacity, shown.zoom, shown.tilt], [1.0, 1.0, 0.0]); | ||
| 2188 | ui.close_popup(menu_id()); | ||
| 2189 | menu_frame(&mut ui, at, None); | ||
| 2190 | assert_eq!(motion(&ui), None, "gone the frame it closes"); | ||
| 2191 | assert!(!ui.wants_frame()); | ||
| 2192 | |||
| 2193 | let mut ui = styled(PopupMotion::Fade([0.15, 0.6])); | ||
| 2194 | let first = motion(&ui).unwrap(); | ||
| 2195 | assert!(first.opacity < 0.2 && first.zoom == 1.0 && first.tilt == 0.0); | ||
| 2196 | for _ in 0..10 { | ||
| 2197 | menu_frame(&mut ui, at, None); | ||
| 2198 | } | ||
| 2199 | assert_eq!(motion(&ui).map(|motion| motion.opacity), Some(1.0)); | ||
| 2200 | ui.close_popup(menu_id()); | ||
| 2201 | // 96 ms into the fade out, OutQuart leaves about half. | ||
| 2202 | for _ in 0..7 { | ||
| 2203 | menu_frame(&mut ui, at, None); | ||
| 2204 | } | ||
| 2205 | let closing = motion(&ui).unwrap(); | ||
| 2206 | assert!((closing.opacity - (1.0 - 0.096 / 0.6_f32).powi(4)).abs() < 0.01); | ||
| 2207 | assert!(ui.wants_frame()); | ||
| 2208 | } |
crates/ui/src/text.rs+8-3| ... | @@ -47,12 +47,17 @@ impl Texts { | ... | @@ -47,12 +47,17 @@ impl Texts { |
| 47 | .context | 47 | .context |
| 48 | .ranged_builder(&mut self.fonts, text, 1.0, false); | 48 | .ranged_builder(&mut self.fonts, text, 1.0, false); |
| 49 | // Fontconfig configurations without a system-ui alias still have a sans-serif. | 49 | // Fontconfig configurations without a system-ui alias still have a sans-serif. |
| 50 | let system = | 50 | // Before 10.10 macOS's interface font is Lucida Grande, which fontique's system-ui |
| 51 | [GenericFamily::SystemUi, GenericFamily::SansSerif].map(FontFamilyName::Generic); | 51 | // doesn't name. |
| 52 | let system = [ | ||
| 53 | Some(FontFamilyName::Generic(GenericFamily::SystemUi)), | ||
| 54 | cfg!(target_os = "macos").then(|| FontFamilyName::named("Lucida Grande")), | ||
| 55 | Some(FontFamilyName::Generic(GenericFamily::SansSerif)), | ||
| 56 | ]; | ||
| 52 | let families: Vec<_> = font | 57 | let families: Vec<_> = font |
| 53 | .into_iter() | 58 | .into_iter() |
| 54 | .map(FontFamilyName::named) | 59 | .map(FontFamilyName::named) |
| 55 | .chain(system) | 60 | .chain(system.into_iter().flatten()) |
| 56 | .collect(); | 61 | .collect(); |
| 57 | builder.push_default(StyleProperty::FontFamily(FontFamily::List(Cow::Owned( | 62 | builder.push_default(StyleProperty::FontFamily(FontFamily::List(Cow::Owned( |
| 58 | families, | 63 | families, |
crates/ui/src/theme.rs+88| ... | @@ -32,6 +32,62 @@ pub struct Theme { | ... | @@ -32,6 +32,62 @@ pub struct Theme { |
| 32 | pub shadow: [f32; 4], | 32 | pub shadow: [f32; 4], |
| 33 | /// Saturation and lightness each of a section's colours takes from its hue. | 33 | /// Saturation and lightness each of a section's colours takes from its hue. |
| 34 | pub shades: Shades, | 34 | pub shades: Shades, |
| 35 | /// Menus as the desktop draws its own, where Snowbound follows it; see `Theme::menu`. | ||
| 36 | pub desktop_menu: Option<Menu>, | ||
| 37 | } | ||
| 38 | |||
| 39 | /// How menus and the other popups look and move. Colours are linear RGBA; sizes are | ||
| 40 | /// logical pixels. | ||
| 41 | #[derive(Clone, Debug, PartialEq)] | ||
| 42 | pub struct Menu { | ||
| 43 | pub fill: [f32; 4], | ||
| 44 | pub border: [f32; 4], | ||
| 45 | /// The layers of the shadow a popup casts, painted in order. | ||
| 46 | pub shadows: Vec<Shadow>, | ||
| 47 | pub text: [f32; 4], | ||
| 48 | /// Shortcuts and headings. | ||
| 49 | pub dim: [f32; 4], | ||
| 50 | pub disabled: [f32; 4], | ||
| 51 | /// The row the pointer or keyboard is on, and its outline. | ||
| 52 | pub highlight: [f32; 4], | ||
| 53 | pub highlight_border: Option<[f32; 4]>, | ||
| 54 | /// The line between groups of rows. | ||
| 55 | pub rule: [f32; 4], | ||
| 56 | pub font_size: f32, | ||
| 57 | pub radius: f32, | ||
| 58 | /// Inset of the popup's contents. | ||
| 59 | pub pad: f32, | ||
| 60 | pub row: f32, | ||
| 61 | pub row_radius: f32, | ||
| 62 | /// Inset of a row's icon and text inside its highlight. | ||
| 63 | pub row_pad: f32, | ||
| 64 | /// Height of the band between groups, and how far the rule across it stays from the | ||
| 65 | /// rows' ends. | ||
| 66 | pub rule_band: f32, | ||
| 67 | pub rule_inset: f32, | ||
| 68 | pub motion: PopupMotion, | ||
| 69 | } | ||
| 70 | |||
| 71 | /// A soft shadow blurred `blur` wide, `drop` below its box and grown `spread` past it, as | ||
| 72 | /// CSS's `box-shadow` takes them. | ||
| 73 | #[derive(Clone, Copy, Debug, PartialEq)] | ||
| 74 | pub struct Shadow { | ||
| 75 | pub color: [f32; 4], | ||
| 76 | pub blur: f32, | ||
| 77 | pub drop: f32, | ||
| 78 | pub spread: f32, | ||
| 79 | } | ||
| 80 | |||
| 81 | /// How popups other than dialogs open and close. | ||
| 82 | #[derive(Clone, Copy, Debug, PartialEq)] | ||
| 83 | pub enum PopupMotion { | ||
| 84 | /// Growing out of what they open from as they fade, Snowbound's own. | ||
| 85 | Grow, | ||
| 86 | /// At once, as GTK 4 shows its popovers. | ||
| 87 | Cut, | ||
| 88 | /// Opacity alone: in linearly over the first seconds, out over the second easing as | ||
| 89 | /// OutQuart, as KWin's Fading Popups. | ||
| 90 | Fade([f32; 2]), | ||
| 35 | } | 91 | } |
| 36 | 92 | ||
| 37 | /// The platform's caret and selection colours in a light or `dark` appearance. | 93 | /// The platform's caret and selection colours in a light or `dark` appearance. |
| ... | @@ -101,6 +157,7 @@ impl Theme { | ... | @@ -101,6 +157,7 @@ impl Theme { |
| 101 | edge: [0.30, 0.16], | 157 | edge: [0.30, 0.16], |
| 102 | accent: [0.55, 0.55], | 158 | accent: [0.55, 0.55], |
| 103 | }, | 159 | }, |
| 160 | desktop_menu: None, | ||
| 104 | } | 161 | } |
| 105 | } | 162 | } |
| 106 | 163 | ||
| ... | @@ -129,6 +186,7 @@ impl Theme { | ... | @@ -129,6 +186,7 @@ impl Theme { |
| 129 | edge: [0.30, 0.52], | 186 | edge: [0.30, 0.52], |
| 130 | accent: [0.60, 0.45], | 187 | accent: [0.60, 0.45], |
| 131 | }, | 188 | }, |
| 189 | desktop_menu: None, | ||
| 132 | } | 190 | } |
| 133 | } | 191 | } |
| 134 | 192 | ||
| ... | @@ -145,6 +203,36 @@ impl Theme { | ... | @@ -145,6 +203,36 @@ impl Theme { |
| 145 | } | 203 | } |
| 146 | } | 204 | } |
| 147 | 205 | ||
| 206 | /// Menus as the desktop draws them, or Snowbound's own. | ||
| 207 | pub fn menu(&self) -> Menu { | ||
| 208 | use crate::popup::{MENU_ROW, PAD}; | ||
| 209 | self.desktop_menu.clone().unwrap_or_else(|| Menu { | ||
| 210 | fill: self.popup, | ||
| 211 | border: self.chip, | ||
| 212 | shadows: vec![Shadow { | ||
| 213 | color: self.shadow, | ||
| 214 | blur: crate::POPUP_SHADOW[0], | ||
| 215 | drop: crate::POPUP_SHADOW[1], | ||
| 216 | spread: 0.0, | ||
| 217 | }], | ||
| 218 | text: self.text, | ||
| 219 | dim: self.text_dim, | ||
| 220 | disabled: self.text_dim, | ||
| 221 | highlight: self.hover(), | ||
| 222 | highlight_border: None, | ||
| 223 | rule: self.chip, | ||
| 224 | font_size: self.font_size, | ||
| 225 | radius: 6.0, | ||
| 226 | pad: PAD, | ||
| 227 | row: MENU_ROW, | ||
| 228 | row_radius: 4.0, | ||
| 229 | row_pad: 8.0, | ||
| 230 | rule_band: 9.0, | ||
| 231 | rule_inset: 0.0, | ||
| 232 | motion: PopupMotion::Grow, | ||
| 233 | }) | ||
| 234 | } | ||
| 235 | |||
| 148 | /// Hovered controls: the accent over the base. | 236 | /// Hovered controls: the accent over the base. |
| 149 | pub fn hover(&self) -> [f32; 4] { | 237 | pub fn hover(&self) -> [f32; 4] { |
| 150 | crate::mix(self.base, self.accent, 0.35) | 238 | crate::mix(self.base, self.accent, 0.35) |
crates/ui/src/widgets.rs+1| ... | @@ -348,6 +348,7 @@ pub fn edit_key(key: &Key) -> edit::Key { | ... | @@ -348,6 +348,7 @@ pub fn edit_key(key: &Key) -> edit::Key { |
| 348 | NamedKey::End => Edit::End, | 348 | NamedKey::End => Edit::End, |
| 349 | NamedKey::PageUp => Edit::PageUp, | 349 | NamedKey::PageUp => Edit::PageUp, |
| 350 | NamedKey::PageDown => Edit::PageDown, | 350 | NamedKey::PageDown => Edit::PageDown, |
| 351 | NamedKey::F11 => Edit::F11, | ||
| 351 | NamedKey::Alt | 352 | NamedKey::Alt |
| 352 | | NamedKey::AltGraph | 353 | | NamedKey::AltGraph |
| 353 | | NamedKey::Control | 354 | | NamedKey::Control |
platform/snow-leopard/Cargo.lock+1377-75| ... | @@ -2,18 +2,72 @@ | ... | @@ -2,18 +2,72 @@ |
| 2 | # It is not intended for manual editing. | 2 | # It is not intended for manual editing. |
| 3 | version = 4 | 3 | version = 4 |
| 4 | 4 | ||
| 5 | [[package]] | ||
| 6 | name = "ab_glyph" | ||
| 7 | version = "0.2.32" | ||
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9 | checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" | ||
| 10 | dependencies = [ | ||
| 11 | "ab_glyph_rasterizer", | ||
| 12 | "owned_ttf_parser", | ||
| 13 | ] | ||
| 14 | |||
| 15 | [[package]] | ||
| 16 | name = "ab_glyph_rasterizer" | ||
| 17 | version = "0.1.10" | ||
| 18 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 19 | checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" | ||
| 20 | |||
| 5 | [[package]] | 21 | [[package]] |
| 6 | name = "adler2" | 22 | name = "adler2" |
| 7 | version = "2.0.1" | 23 | version = "2.0.1" |
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | 24 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" | 25 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" |
| 10 | 26 | ||
| 27 | [[package]] | ||
| 28 | name = "ahash" | ||
| 29 | version = "0.8.12" | ||
| 30 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 31 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" | ||
| 32 | dependencies = [ | ||
| 33 | "cfg-if", | ||
| 34 | "getrandom 0.3.4", | ||
| 35 | "once_cell", | ||
| 36 | "version_check", | ||
| 37 | "zerocopy", | ||
| 38 | ] | ||
| 39 | |||
| 11 | [[package]] | 40 | [[package]] |
| 12 | name = "allocator-api2" | 41 | name = "allocator-api2" |
| 13 | version = "0.2.21" | 42 | version = "0.2.21" |
| 14 | source = "registry+https://github.com/rust-lang/crates.io-index" | 43 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 15 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | 44 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" |
| 16 | 45 | ||
| 46 | [[package]] | ||
| 47 | name = "android-activity" | ||
| 48 | version = "0.6.1" | ||
| 49 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 50 | checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" | ||
| 51 | dependencies = [ | ||
| 52 | "android-properties", | ||
| 53 | "bitflags 2.13.1", | ||
| 54 | "cc", | ||
| 55 | "jni", | ||
| 56 | "libc", | ||
| 57 | "log", | ||
| 58 | "ndk", | ||
| 59 | "ndk-context", | ||
| 60 | "ndk-sys", | ||
| 61 | "num_enum", | ||
| 62 | "thiserror 2.0.21", | ||
| 63 | ] | ||
| 64 | |||
| 65 | [[package]] | ||
| 66 | name = "android-properties" | ||
| 67 | version = "0.2.2" | ||
| 68 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 69 | checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" | ||
| 70 | |||
| 17 | [[package]] | 71 | [[package]] |
| 18 | name = "android_system_properties" | 72 | name = "android_system_properties" |
| 19 | version = "0.1.6" | 73 | version = "0.1.6" |
| ... | @@ -23,12 +77,24 @@ dependencies = [ | ... | @@ -23,12 +77,24 @@ dependencies = [ |
| 23 | "libc", | 77 | "libc", |
| 24 | ] | 78 | ] |
| 25 | 79 | ||
| 80 | [[package]] | ||
| 81 | name = "arrayref" | ||
| 82 | version = "0.3.9" | ||
| 83 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 84 | checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" | ||
| 85 | |||
| 26 | [[package]] | 86 | [[package]] |
| 27 | name = "arrayvec" | 87 | name = "arrayvec" |
| 28 | version = "0.7.8" | 88 | version = "0.7.8" |
| 29 | source = "registry+https://github.com/rust-lang/crates.io-index" | 89 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 30 | checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" | 90 | checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" |
| 31 | 91 | ||
| 92 | [[package]] | ||
| 93 | name = "as-raw-xcb-connection" | ||
| 94 | version = "1.0.1" | ||
| 95 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 96 | checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" | ||
| 97 | |||
| 32 | [[package]] | 98 | [[package]] |
| 33 | name = "ash" | 99 | name = "ash" |
| 34 | version = "0.38.0+1.3.281" | 100 | version = "0.38.0+1.3.281" |
| ... | @@ -38,6 +104,12 @@ dependencies = [ | ... | @@ -38,6 +104,12 @@ dependencies = [ |
| 38 | "libloading", | 104 | "libloading", |
| 39 | ] | 105 | ] |
| 40 | 106 | ||
| 107 | [[package]] | ||
| 108 | name = "atomic-waker" | ||
| 109 | version = "1.1.2" | ||
| 110 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 111 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" | ||
| 112 | |||
| 41 | [[package]] | 113 | [[package]] |
| 42 | name = "autocfg" | 114 | name = "autocfg" |
| 43 | version = "1.5.1" | 115 | version = "1.5.1" |
| ... | @@ -59,19 +131,34 @@ version = "0.9.1" | ... | @@ -59,19 +131,34 @@ version = "0.9.1" |
| 59 | source = "registry+https://github.com/rust-lang/crates.io-index" | 131 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 60 | checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" | 132 | checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" |
| 61 | 133 | ||
| 134 | [[package]] | ||
| 135 | name = "bitflags" | ||
| 136 | version = "1.3.2" | ||
| 137 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 138 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | ||
| 139 | |||
| 62 | [[package]] | 140 | [[package]] |
| 63 | name = "bitflags" | 141 | name = "bitflags" |
| 64 | version = "2.13.1" | 142 | version = "2.13.1" |
| 65 | source = "registry+https://github.com/rust-lang/crates.io-index" | 143 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 66 | checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" | 144 | checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" |
| 67 | 145 | ||
| 146 | [[package]] | ||
| 147 | name = "block2" | ||
| 148 | version = "0.5.1" | ||
| 149 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 150 | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" | ||
| 151 | dependencies = [ | ||
| 152 | "objc2 0.5.2", | ||
| 153 | ] | ||
| 154 | |||
| 68 | [[package]] | 155 | [[package]] |
| 69 | name = "block2" | 156 | name = "block2" |
| 70 | version = "0.6.2" | 157 | version = "0.6.2" |
| 71 | source = "registry+https://github.com/rust-lang/crates.io-index" | 158 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 72 | checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" | 159 | checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" |
| 73 | dependencies = [ | 160 | dependencies = [ |
| 74 | "objc2", | 161 | "objc2 0.6.4", |
| 75 | ] | 162 | ] |
| 76 | 163 | ||
| 77 | [[package]] | 164 | [[package]] |
| ... | @@ -106,6 +193,50 @@ version = "0.1.0" | ... | @@ -106,6 +193,50 @@ version = "0.1.0" |
| 106 | source = "registry+https://github.com/rust-lang/crates.io-index" | 193 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 107 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" | 194 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" |
| 108 | 195 | ||
| 196 | [[package]] | ||
| 197 | name = "bytes" | ||
| 198 | version = "1.12.1" | ||
| 199 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 200 | checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" | ||
| 201 | |||
| 202 | [[package]] | ||
| 203 | name = "calloop" | ||
| 204 | version = "0.13.0" | ||
| 205 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 206 | checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" | ||
| 207 | dependencies = [ | ||
| 208 | "bitflags 2.13.1", | ||
| 209 | "log", | ||
| 210 | "polling", | ||
| 211 | "rustix 0.38.44", | ||
| 212 | "slab", | ||
| 213 | "thiserror 1.0.69", | ||
| 214 | ] | ||
| 215 | |||
| 216 | [[package]] | ||
| 217 | name = "calloop-wayland-source" | ||
| 218 | version = "0.3.0" | ||
| 219 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 220 | checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" | ||
| 221 | dependencies = [ | ||
| 222 | "calloop", | ||
| 223 | "rustix 0.38.44", | ||
| 224 | "wayland-backend", | ||
| 225 | "wayland-client", | ||
| 226 | ] | ||
| 227 | |||
| 228 | [[package]] | ||
| 229 | name = "cc" | ||
| 230 | version = "1.5.1" | ||
| 231 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 232 | checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" | ||
| 233 | dependencies = [ | ||
| 234 | "find-msvc-tools", | ||
| 235 | "jobserver", | ||
| 236 | "libc", | ||
| 237 | "shlex", | ||
| 238 | ] | ||
| 239 | |||
| 109 | [[package]] | 240 | [[package]] |
| 110 | name = "cfg-if" | 241 | name = "cfg-if" |
| 111 | version = "1.0.5" | 242 | version = "1.0.5" |
| ... | @@ -135,6 +266,65 @@ version = "1.1.0" | ... | @@ -135,6 +266,65 @@ version = "1.1.0" |
| 135 | source = "registry+https://github.com/rust-lang/crates.io-index" | 266 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 136 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" | 267 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" |
| 137 | 268 | ||
| 269 | [[package]] | ||
| 270 | name = "combine" | ||
| 271 | version = "4.6.8" | ||
| 272 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 273 | checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" | ||
| 274 | dependencies = [ | ||
| 275 | "bytes", | ||
| 276 | "memchr", | ||
| 277 | ] | ||
| 278 | |||
| 279 | [[package]] | ||
| 280 | name = "concurrent-queue" | ||
| 281 | version = "2.5.0" | ||
| 282 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 283 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" | ||
| 284 | dependencies = [ | ||
| 285 | "crossbeam-utils", | ||
| 286 | ] | ||
| 287 | |||
| 288 | [[package]] | ||
| 289 | name = "core-foundation" | ||
| 290 | version = "0.9.4" | ||
| 291 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 292 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" | ||
| 293 | dependencies = [ | ||
| 294 | "core-foundation-sys", | ||
| 295 | "libc", | ||
| 296 | ] | ||
| 297 | |||
| 298 | [[package]] | ||
| 299 | name = "core-foundation-sys" | ||
| 300 | version = "0.8.7" | ||
| 301 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 302 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | ||
| 303 | |||
| 304 | [[package]] | ||
| 305 | name = "core-graphics" | ||
| 306 | version = "0.23.2" | ||
| 307 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 308 | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" | ||
| 309 | dependencies = [ | ||
| 310 | "bitflags 1.3.2", | ||
| 311 | "core-foundation", | ||
| 312 | "core-graphics-types", | ||
| 313 | "foreign-types", | ||
| 314 | "libc", | ||
| 315 | ] | ||
| 316 | |||
| 317 | [[package]] | ||
| 318 | name = "core-graphics-types" | ||
| 319 | version = "0.1.3" | ||
| 320 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 321 | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" | ||
| 322 | dependencies = [ | ||
| 323 | "bitflags 1.3.2", | ||
| 324 | "core-foundation", | ||
| 325 | "libc", | ||
| 326 | ] | ||
| 327 | |||
| 138 | [[package]] | 328 | [[package]] |
| 139 | name = "crc32fast" | 329 | name = "crc32fast" |
| 140 | version = "1.5.2" | 330 | version = "1.5.2" |
| ... | @@ -144,20 +334,38 @@ dependencies = [ | ... | @@ -144,20 +334,38 @@ dependencies = [ |
| 144 | "cfg-if", | 334 | "cfg-if", |
| 145 | ] | 335 | ] |
| 146 | 336 | ||
| 337 | [[package]] | ||
| 338 | name = "crossbeam-utils" | ||
| 339 | version = "0.8.23" | ||
| 340 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 341 | checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" | ||
| 342 | |||
| 147 | [[package]] | 343 | [[package]] |
| 148 | name = "crunchy" | 344 | name = "crunchy" |
| 149 | version = "0.2.4" | 345 | version = "0.2.4" |
| 150 | source = "registry+https://github.com/rust-lang/crates.io-index" | 346 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 151 | checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" | 347 | checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" |
| 152 | 348 | ||
| 349 | [[package]] | ||
| 350 | name = "cursor-icon" | ||
| 351 | version = "1.2.0" | ||
| 352 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 353 | checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" | ||
| 354 | |||
| 355 | [[package]] | ||
| 356 | name = "dispatch" | ||
| 357 | version = "0.2.0" | ||
| 358 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 359 | checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" | ||
| 360 | |||
| 153 | [[package]] | 361 | [[package]] |
| 154 | name = "dispatch2" | 362 | name = "dispatch2" |
| 155 | version = "0.3.1" | 363 | version = "0.3.1" |
| 156 | source = "registry+https://github.com/rust-lang/crates.io-index" | 364 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 157 | checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" | 365 | checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" |
| 158 | dependencies = [ | 366 | dependencies = [ |
| 159 | "bitflags", | 367 | "bitflags 2.13.1", |
| 160 | "objc2", | 368 | "objc2 0.6.4", |
| 161 | ] | 369 | ] |
| 162 | 370 | ||
| 163 | [[package]] | 371 | [[package]] |
| ... | @@ -189,6 +397,18 @@ dependencies = [ | ... | @@ -189,6 +397,18 @@ dependencies = [ |
| 189 | "litrs", | 397 | "litrs", |
| 190 | ] | 398 | ] |
| 191 | 399 | ||
| 400 | [[package]] | ||
| 401 | name = "downcast-rs" | ||
| 402 | version = "1.2.1" | ||
| 403 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 404 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" | ||
| 405 | |||
| 406 | [[package]] | ||
| 407 | name = "dpi" | ||
| 408 | version = "0.1.2" | ||
| 409 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 410 | checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" | ||
| 411 | |||
| 192 | [[package]] | 412 | [[package]] |
| 193 | name = "draw" | 413 | name = "draw" |
| 194 | version = "0.1.0" | 414 | version = "0.1.0" |
| ... | @@ -208,6 +428,16 @@ version = "1.0.2" | ... | @@ -208,6 +428,16 @@ version = "1.0.2" |
| 208 | source = "registry+https://github.com/rust-lang/crates.io-index" | 428 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 209 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | 429 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" |
| 210 | 430 | ||
| 431 | [[package]] | ||
| 432 | name = "errno" | ||
| 433 | version = "0.3.14" | ||
| 434 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 435 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | ||
| 436 | dependencies = [ | ||
| 437 | "libc", | ||
| 438 | "windows-sys 0.61.2", | ||
| 439 | ] | ||
| 440 | |||
| 211 | [[package]] | 441 | [[package]] |
| 212 | name = "fax" | 442 | name = "fax" |
| 213 | version = "0.2.7" | 443 | version = "0.2.7" |
| ... | @@ -223,6 +453,12 @@ dependencies = [ | ... | @@ -223,6 +453,12 @@ dependencies = [ |
| 223 | "simd-adler32", | 453 | "simd-adler32", |
| 224 | ] | 454 | ] |
| 225 | 455 | ||
| 456 | [[package]] | ||
| 457 | name = "find-msvc-tools" | ||
| 458 | version = "0.1.14" | ||
| 459 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 460 | checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" | ||
| 461 | |||
| 226 | [[package]] | 462 | [[package]] |
| 227 | name = "flate2" | 463 | name = "flate2" |
| 228 | version = "1.1.10" | 464 | version = "1.1.10" |
| ... | @@ -257,10 +493,10 @@ dependencies = [ | ... | @@ -257,10 +493,10 @@ dependencies = [ |
| 257 | "hashbrown 0.17.1", | 493 | "hashbrown 0.17.1", |
| 258 | "linebender_resource_handle", | 494 | "linebender_resource_handle", |
| 259 | "memmap2", | 495 | "memmap2", |
| 260 | "objc2", | 496 | "objc2 0.6.4", |
| 261 | "objc2-core-foundation", | 497 | "objc2-core-foundation", |
| 262 | "objc2-core-text", | 498 | "objc2-core-text", |
| 263 | "objc2-foundation", | 499 | "objc2-foundation 0.3.2", |
| 264 | "parlance", | 500 | "parlance", |
| 265 | "read-fonts", | 501 | "read-fonts", |
| 266 | "roxmltree", | 502 | "roxmltree", |
| ... | @@ -270,6 +506,33 @@ dependencies = [ | ... | @@ -270,6 +506,33 @@ dependencies = [ |
| 270 | "yeslogic-fontconfig-sys", | 506 | "yeslogic-fontconfig-sys", |
| 271 | ] | 507 | ] |
| 272 | 508 | ||
| 509 | [[package]] | ||
| 510 | name = "foreign-types" | ||
| 511 | version = "0.5.0" | ||
| 512 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 513 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" | ||
| 514 | dependencies = [ | ||
| 515 | "foreign-types-macros", | ||
| 516 | "foreign-types-shared", | ||
| 517 | ] | ||
| 518 | |||
| 519 | [[package]] | ||
| 520 | name = "foreign-types-macros" | ||
| 521 | version = "0.2.4" | ||
| 522 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 523 | checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" | ||
| 524 | dependencies = [ | ||
| 525 | "proc-macro2", | ||
| 526 | "quote", | ||
| 527 | "syn 3.0.6", | ||
| 528 | ] | ||
| 529 | |||
| 530 | [[package]] | ||
| 531 | name = "foreign-types-shared" | ||
| 532 | version = "0.3.1" | ||
| 533 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 534 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" | ||
| 535 | |||
| 273 | [[package]] | 536 | [[package]] |
| 274 | name = "futures-core" | 537 | name = "futures-core" |
| 275 | version = "0.3.34" | 538 | version = "0.3.34" |
| ... | @@ -294,6 +557,39 @@ dependencies = [ | ... | @@ -294,6 +557,39 @@ dependencies = [ |
| 294 | "slab", | 557 | "slab", |
| 295 | ] | 558 | ] |
| 296 | 559 | ||
| 560 | [[package]] | ||
| 561 | name = "gethostname" | ||
| 562 | version = "1.1.0" | ||
| 563 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 564 | checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" | ||
| 565 | dependencies = [ | ||
| 566 | "rustix 1.1.5", | ||
| 567 | "windows-link", | ||
| 568 | ] | ||
| 569 | |||
| 570 | [[package]] | ||
| 571 | name = "getrandom" | ||
| 572 | version = "0.3.4" | ||
| 573 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 574 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | ||
| 575 | dependencies = [ | ||
| 576 | "cfg-if", | ||
| 577 | "libc", | ||
| 578 | "r-efi 5.3.0", | ||
| 579 | "wasip2", | ||
| 580 | ] | ||
| 581 | |||
| 582 | [[package]] | ||
| 583 | name = "getrandom" | ||
| 584 | version = "0.4.3" | ||
| 585 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 586 | checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" | ||
| 587 | dependencies = [ | ||
| 588 | "cfg-if", | ||
| 589 | "libc", | ||
| 590 | "r-efi 6.0.0", | ||
| 591 | ] | ||
| 592 | |||
| 297 | [[package]] | 593 | [[package]] |
| 298 | name = "gif" | 594 | name = "gif" |
| 299 | version = "0.14.2" | 595 | version = "0.14.2" |
| ... | @@ -304,10 +600,6 @@ dependencies = [ | ... | @@ -304,10 +600,6 @@ dependencies = [ |
| 304 | "weezl", | 600 | "weezl", |
| 305 | ] | 601 | ] |
| 306 | 602 | ||
| 307 | [[package]] | ||
| 308 | name = "gl21" | ||
| 309 | version = "0.0.0" | ||
| 310 | |||
| 311 | [[package]] | 603 | [[package]] |
| 312 | name = "gl_generator" | 604 | name = "gl_generator" |
| 313 | version = "0.14.0" | 605 | version = "0.14.0" |
| ... | @@ -350,7 +642,7 @@ dependencies = [ | ... | @@ -350,7 +642,7 @@ dependencies = [ |
| 350 | "hashbrown 0.16.1", | 642 | "hashbrown 0.16.1", |
| 351 | "log", | 643 | "log", |
| 352 | "presser", | 644 | "presser", |
| 353 | "thiserror", | 645 | "thiserror 2.0.21", |
| 354 | "windows", | 646 | "windows", |
| 355 | ] | 647 | ] |
| 356 | 648 | ||
| ... | @@ -372,7 +664,7 @@ version = "0.12.0" | ... | @@ -372,7 +664,7 @@ version = "0.12.0" |
| 372 | source = "registry+https://github.com/rust-lang/crates.io-index" | 664 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 373 | checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" | 665 | checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" |
| 374 | dependencies = [ | 666 | dependencies = [ |
| 375 | "bitflags", | 667 | "bitflags 2.13.1", |
| 376 | "bytemuck", | 668 | "bytemuck", |
| 377 | "read-fonts", | 669 | "read-fonts", |
| 378 | "smallvec", | 670 | "smallvec", |
| ... | @@ -402,6 +694,12 @@ dependencies = [ | ... | @@ -402,6 +694,12 @@ dependencies = [ |
| 402 | name = "hello-snow-leopard" | 694 | name = "hello-snow-leopard" |
| 403 | version = "0.0.0" | 695 | version = "0.0.0" |
| 404 | 696 | ||
| 697 | [[package]] | ||
| 698 | name = "hermit-abi" | ||
| 699 | version = "0.5.3" | ||
| 700 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 701 | checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" | ||
| 702 | |||
| 405 | [[package]] | 703 | [[package]] |
| 406 | name = "icu_collections" | 704 | name = "icu_collections" |
| 407 | version = "2.3.0" | 705 | version = "2.3.0" |
| ... | @@ -558,6 +856,36 @@ dependencies = [ | ... | @@ -558,6 +856,36 @@ dependencies = [ |
| 558 | "hashbrown 0.17.1", | 856 | "hashbrown 0.17.1", |
| 559 | ] | 857 | ] |
| 560 | 858 | ||
| 859 | [[package]] | ||
| 860 | name = "jni" | ||
| 861 | version = "0.22.4" | ||
| 862 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 863 | checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" | ||
| 864 | dependencies = [ | ||
| 865 | "cfg-if", | ||
| 866 | "combine", | ||
| 867 | "jni-macros", | ||
| 868 | "jni-sys 0.4.1", | ||
| 869 | "log", | ||
| 870 | "simd_cesu8", | ||
| 871 | "thiserror 2.0.21", | ||
| 872 | "walkdir", | ||
| 873 | "windows-link", | ||
| 874 | ] | ||
| 875 | |||
| 876 | [[package]] | ||
| 877 | name = "jni-macros" | ||
| 878 | version = "0.22.4" | ||
| 879 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 880 | checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" | ||
| 881 | dependencies = [ | ||
| 882 | "proc-macro2", | ||
| 883 | "quote", | ||
| 884 | "rustc_version", | ||
| 885 | "simd_cesu8", | ||
| 886 | "syn 2.0.119", | ||
| 887 | ] | ||
| 888 | |||
| 561 | [[package]] | 889 | [[package]] |
| 562 | name = "jni-sys" | 890 | name = "jni-sys" |
| 563 | version = "0.3.1" | 891 | version = "0.3.1" |
| ... | @@ -586,6 +914,16 @@ dependencies = [ | ... | @@ -586,6 +914,16 @@ dependencies = [ |
| 586 | "syn 2.0.119", | 914 | "syn 2.0.119", |
| 587 | ] | 915 | ] |
| 588 | 916 | ||
| 917 | [[package]] | ||
| 918 | name = "jobserver" | ||
| 919 | version = "0.1.35" | ||
| 920 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 921 | checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" | ||
| 922 | dependencies = [ | ||
| 923 | "getrandom 0.4.3", | ||
| 924 | "libc", | ||
| 925 | ] | ||
| 926 | |||
| 589 | [[package]] | 927 | [[package]] |
| 590 | name = "js-sys" | 928 | name = "js-sys" |
| 591 | version = "0.3.105" | 929 | version = "0.3.105" |
| ... | @@ -636,12 +974,36 @@ version = "0.2.16" | ... | @@ -636,12 +974,36 @@ version = "0.2.16" |
| 636 | source = "registry+https://github.com/rust-lang/crates.io-index" | 974 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 637 | checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" | 975 | checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" |
| 638 | 976 | ||
| 977 | [[package]] | ||
| 978 | name = "libredox" | ||
| 979 | version = "0.1.25" | ||
| 980 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 981 | checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" | ||
| 982 | dependencies = [ | ||
| 983 | "bitflags 2.13.1", | ||
| 984 | "libc", | ||
| 985 | "plain", | ||
| 986 | "redox_syscall 0.9.4", | ||
| 987 | ] | ||
| 988 | |||
| 639 | [[package]] | 989 | [[package]] |
| 640 | name = "linebender_resource_handle" | 990 | name = "linebender_resource_handle" |
| 641 | version = "0.1.1" | 991 | version = "0.1.1" |
| 642 | source = "registry+https://github.com/rust-lang/crates.io-index" | 992 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 643 | checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" | 993 | checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" |
| 644 | 994 | ||
| 995 | [[package]] | ||
| 996 | name = "linux-raw-sys" | ||
| 997 | version = "0.4.15" | ||
| 998 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 999 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" | ||
| 1000 | |||
| 1001 | [[package]] | ||
| 1002 | name = "linux-raw-sys" | ||
| 1003 | version = "0.12.1" | ||
| 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1005 | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | ||
| 1006 | |||
| 645 | [[package]] | 1007 | [[package]] |
| 646 | name = "litemap" | 1008 | name = "litemap" |
| 647 | version = "0.8.3" | 1009 | version = "0.8.3" |
| ... | @@ -722,7 +1084,7 @@ checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" | ... | @@ -722,7 +1084,7 @@ checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" |
| 722 | dependencies = [ | 1084 | dependencies = [ |
| 723 | "arrayvec", | 1085 | "arrayvec", |
| 724 | "bit-set", | 1086 | "bit-set", |
| 725 | "bitflags", | 1087 | "bitflags 2.13.1", |
| 726 | "cfg-if", | 1088 | "cfg-if", |
| 727 | "cfg_aliases", | 1089 | "cfg_aliases", |
| 728 | "codespan-reporting", | 1090 | "codespan-reporting", |
| ... | @@ -736,7 +1098,7 @@ dependencies = [ | ... | @@ -736,7 +1098,7 @@ dependencies = [ |
| 736 | "once_cell", | 1098 | "once_cell", |
| 737 | "rustc-hash", | 1099 | "rustc-hash", |
| 738 | "spirv", | 1100 | "spirv", |
| 739 | "thiserror", | 1101 | "thiserror 2.0.21", |
| 740 | "unicode-ident", | 1102 | "unicode-ident", |
| 741 | ] | 1103 | ] |
| 742 | 1104 | ||
| ... | @@ -749,9 +1111,30 @@ dependencies = [ | ... | @@ -749,9 +1111,30 @@ dependencies = [ |
| 749 | "hashbrown 0.17.1", | 1111 | "hashbrown 0.17.1", |
| 750 | "indexmap", | 1112 | "indexmap", |
| 751 | "rustc-hash", | 1113 | "rustc-hash", |
| 752 | "thiserror", | 1114 | "thiserror 2.0.21", |
| 1115 | ] | ||
| 1116 | |||
| 1117 | [[package]] | ||
| 1118 | name = "ndk" | ||
| 1119 | version = "0.9.0" | ||
| 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1121 | checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" | ||
| 1122 | dependencies = [ | ||
| 1123 | "bitflags 2.13.1", | ||
| 1124 | "jni-sys 0.3.1", | ||
| 1125 | "log", | ||
| 1126 | "ndk-sys", | ||
| 1127 | "num_enum", | ||
| 1128 | "raw-window-handle", | ||
| 1129 | "thiserror 1.0.69", | ||
| 753 | ] | 1130 | ] |
| 754 | 1131 | ||
| 1132 | [[package]] | ||
| 1133 | name = "ndk-context" | ||
| 1134 | version = "0.1.1" | ||
| 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1136 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" | ||
| 1137 | |||
| 755 | [[package]] | 1138 | [[package]] |
| 756 | name = "ndk-sys" | 1139 | name = "ndk-sys" |
| 757 | version = "0.6.0+11769913" | 1140 | version = "0.6.0+11769913" |
| ... | @@ -761,6 +1144,16 @@ dependencies = [ | ... | @@ -761,6 +1144,16 @@ dependencies = [ |
| 761 | "jni-sys 0.3.1", | 1144 | "jni-sys 0.3.1", |
| 762 | ] | 1145 | ] |
| 763 | 1146 | ||
| 1147 | [[package]] | ||
| 1148 | name = "nucleo-matcher" | ||
| 1149 | version = "0.3.1" | ||
| 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1151 | checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" | ||
| 1152 | dependencies = [ | ||
| 1153 | "memchr", | ||
| 1154 | "unicode-segmentation", | ||
| 1155 | ] | ||
| 1156 | |||
| 764 | [[package]] | 1157 | [[package]] |
| 765 | name = "num-traits" | 1158 | name = "num-traits" |
| 766 | version = "0.2.19" | 1159 | version = "0.2.19" |
| ... | @@ -772,55 +1165,168 @@ dependencies = [ | ... | @@ -772,55 +1165,168 @@ dependencies = [ |
| 772 | ] | 1165 | ] |
| 773 | 1166 | ||
| 774 | [[package]] | 1167 | [[package]] |
| 775 | name = "objc2" | 1168 | name = "num_enum" |
| 776 | version = "0.6.4" | 1169 | version = "0.7.6" |
| 777 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 778 | checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" | 1171 | checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" |
| 779 | dependencies = [ | 1172 | dependencies = [ |
| 780 | "objc2-encode", | 1173 | "num_enum_derive", |
| 1174 | "rustversion", | ||
| 781 | ] | 1175 | ] |
| 782 | 1176 | ||
| 783 | [[package]] | 1177 | [[package]] |
| 784 | name = "objc2-app-kit" | 1178 | name = "num_enum_derive" |
| 785 | version = "0.3.2" | 1179 | version = "0.7.6" |
| 786 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 787 | checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" | 1181 | checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" |
| 788 | dependencies = [ | 1182 | dependencies = [ |
| 789 | "bitflags", | 1183 | "proc-macro-crate", |
| 790 | "objc2", | 1184 | "proc-macro2", |
| 791 | "objc2-foundation", | 1185 | "quote", |
| 1186 | "syn 2.0.119", | ||
| 792 | ] | 1187 | ] |
| 793 | 1188 | ||
| 794 | [[package]] | 1189 | [[package]] |
| 795 | name = "objc2-core-foundation" | 1190 | name = "objc-sys" |
| 796 | version = "0.3.2" | 1191 | version = "0.3.5" |
| 797 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 798 | checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" | 1193 | checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" |
| 799 | dependencies = [ | ||
| 800 | "bitflags", | ||
| 801 | "dispatch2", | ||
| 802 | "objc2", | ||
| 803 | ] | ||
| 804 | 1194 | ||
| 805 | [[package]] | 1195 | [[package]] |
| 806 | name = "objc2-core-graphics" | 1196 | name = "objc2" |
| 807 | version = "0.3.2" | 1197 | version = "0.5.2" |
| 808 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 809 | checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" | 1199 | checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" |
| 810 | dependencies = [ | 1200 | dependencies = [ |
| 811 | "dispatch2", | 1201 | "objc-sys", |
| 812 | "objc2", | 1202 | "objc2-encode", |
| 813 | "objc2-core-foundation", | 1203 | ] |
| 1204 | |||
| 1205 | [[package]] | ||
| 1206 | name = "objc2" | ||
| 1207 | version = "0.6.4" | ||
| 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1209 | checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" | ||
| 1210 | dependencies = [ | ||
| 1211 | "objc2-encode", | ||
| 1212 | ] | ||
| 1213 | |||
| 1214 | [[package]] | ||
| 1215 | name = "objc2-app-kit" | ||
| 1216 | version = "0.2.2" | ||
| 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1218 | checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" | ||
| 1219 | dependencies = [ | ||
| 1220 | "bitflags 2.13.1", | ||
| 1221 | "block2 0.5.1", | ||
| 1222 | "libc", | ||
| 1223 | "objc2 0.5.2", | ||
| 1224 | "objc2-core-data", | ||
| 1225 | "objc2-core-image", | ||
| 1226 | "objc2-foundation 0.2.2", | ||
| 1227 | "objc2-quartz-core 0.2.2", | ||
| 1228 | ] | ||
| 1229 | |||
| 1230 | [[package]] | ||
| 1231 | name = "objc2-app-kit" | ||
| 1232 | version = "0.3.2" | ||
| 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1234 | checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" | ||
| 1235 | dependencies = [ | ||
| 1236 | "objc2 0.6.4", | ||
| 1237 | "objc2-foundation 0.3.2", | ||
| 1238 | ] | ||
| 1239 | |||
| 1240 | [[package]] | ||
| 1241 | name = "objc2-cloud-kit" | ||
| 1242 | version = "0.2.2" | ||
| 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1244 | checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" | ||
| 1245 | dependencies = [ | ||
| 1246 | "bitflags 2.13.1", | ||
| 1247 | "block2 0.5.1", | ||
| 1248 | "objc2 0.5.2", | ||
| 1249 | "objc2-core-location", | ||
| 1250 | "objc2-foundation 0.2.2", | ||
| 1251 | ] | ||
| 1252 | |||
| 1253 | [[package]] | ||
| 1254 | name = "objc2-contacts" | ||
| 1255 | version = "0.2.2" | ||
| 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1257 | checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" | ||
| 1258 | dependencies = [ | ||
| 1259 | "block2 0.5.1", | ||
| 1260 | "objc2 0.5.2", | ||
| 1261 | "objc2-foundation 0.2.2", | ||
| 1262 | ] | ||
| 1263 | |||
| 1264 | [[package]] | ||
| 1265 | name = "objc2-core-data" | ||
| 1266 | version = "0.2.2" | ||
| 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1268 | checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" | ||
| 1269 | dependencies = [ | ||
| 1270 | "bitflags 2.13.1", | ||
| 1271 | "block2 0.5.1", | ||
| 1272 | "objc2 0.5.2", | ||
| 1273 | "objc2-foundation 0.2.2", | ||
| 1274 | ] | ||
| 1275 | |||
| 1276 | [[package]] | ||
| 1277 | name = "objc2-core-foundation" | ||
| 1278 | version = "0.3.2" | ||
| 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1280 | checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" | ||
| 1281 | dependencies = [ | ||
| 1282 | "bitflags 2.13.1", | ||
| 1283 | "dispatch2", | ||
| 1284 | "objc2 0.6.4", | ||
| 1285 | ] | ||
| 1286 | |||
| 1287 | [[package]] | ||
| 1288 | name = "objc2-core-graphics" | ||
| 1289 | version = "0.3.2" | ||
| 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1291 | checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" | ||
| 1292 | dependencies = [ | ||
| 1293 | "dispatch2", | ||
| 1294 | "objc2 0.6.4", | ||
| 1295 | "objc2-core-foundation", | ||
| 814 | "objc2-io-surface", | 1296 | "objc2-io-surface", |
| 815 | ] | 1297 | ] |
| 816 | 1298 | ||
| 1299 | [[package]] | ||
| 1300 | name = "objc2-core-image" | ||
| 1301 | version = "0.2.2" | ||
| 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1303 | checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" | ||
| 1304 | dependencies = [ | ||
| 1305 | "block2 0.5.1", | ||
| 1306 | "objc2 0.5.2", | ||
| 1307 | "objc2-foundation 0.2.2", | ||
| 1308 | "objc2-metal 0.2.2", | ||
| 1309 | ] | ||
| 1310 | |||
| 1311 | [[package]] | ||
| 1312 | name = "objc2-core-location" | ||
| 1313 | version = "0.2.2" | ||
| 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1315 | checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" | ||
| 1316 | dependencies = [ | ||
| 1317 | "block2 0.5.1", | ||
| 1318 | "objc2 0.5.2", | ||
| 1319 | "objc2-contacts", | ||
| 1320 | "objc2-foundation 0.2.2", | ||
| 1321 | ] | ||
| 1322 | |||
| 817 | [[package]] | 1323 | [[package]] |
| 818 | name = "objc2-core-text" | 1324 | name = "objc2-core-text" |
| 819 | version = "0.3.2" | 1325 | version = "0.3.2" |
| 820 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 821 | checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" | 1327 | checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" |
| 822 | dependencies = [ | 1328 | dependencies = [ |
| 823 | "bitflags", | 1329 | "bitflags 2.13.1", |
| 824 | "objc2-core-foundation", | 1330 | "objc2-core-foundation", |
| 825 | ] | 1331 | ] |
| 826 | 1332 | ||
| ... | @@ -830,14 +1336,27 @@ version = "4.1.0" | ... | @@ -830,14 +1336,27 @@ version = "4.1.0" |
| 830 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 831 | checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" | 1337 | checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" |
| 832 | 1338 | ||
| 1339 | [[package]] | ||
| 1340 | name = "objc2-foundation" | ||
| 1341 | version = "0.2.2" | ||
| 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1343 | checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" | ||
| 1344 | dependencies = [ | ||
| 1345 | "bitflags 2.13.1", | ||
| 1346 | "block2 0.5.1", | ||
| 1347 | "dispatch", | ||
| 1348 | "libc", | ||
| 1349 | "objc2 0.5.2", | ||
| 1350 | ] | ||
| 1351 | |||
| 833 | [[package]] | 1352 | [[package]] |
| 834 | name = "objc2-foundation" | 1353 | name = "objc2-foundation" |
| 835 | version = "0.3.2" | 1354 | version = "0.3.2" |
| 836 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 837 | checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" | 1356 | checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" |
| 838 | dependencies = [ | 1357 | dependencies = [ |
| 839 | "bitflags", | 1358 | "bitflags 2.13.1", |
| 840 | "objc2", | 1359 | "objc2 0.6.4", |
| 841 | "objc2-core-foundation", | 1360 | "objc2-core-foundation", |
| 842 | ] | 1361 | ] |
| 843 | 1362 | ||
| ... | @@ -847,21 +1366,58 @@ version = "0.3.2" | ... | @@ -847,21 +1366,58 @@ version = "0.3.2" |
| 847 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 848 | checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" | 1367 | checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" |
| 849 | dependencies = [ | 1368 | dependencies = [ |
| 850 | "bitflags", | 1369 | "bitflags 2.13.1", |
| 851 | "objc2", | 1370 | "objc2 0.6.4", |
| 852 | "objc2-core-foundation", | 1371 | "objc2-core-foundation", |
| 853 | ] | 1372 | ] |
| 854 | 1373 | ||
| 1374 | [[package]] | ||
| 1375 | name = "objc2-link-presentation" | ||
| 1376 | version = "0.2.2" | ||
| 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1378 | checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" | ||
| 1379 | dependencies = [ | ||
| 1380 | "block2 0.5.1", | ||
| 1381 | "objc2 0.5.2", | ||
| 1382 | "objc2-app-kit 0.2.2", | ||
| 1383 | "objc2-foundation 0.2.2", | ||
| 1384 | ] | ||
| 1385 | |||
| 1386 | [[package]] | ||
| 1387 | name = "objc2-metal" | ||
| 1388 | version = "0.2.2" | ||
| 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1390 | checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" | ||
| 1391 | dependencies = [ | ||
| 1392 | "bitflags 2.13.1", | ||
| 1393 | "block2 0.5.1", | ||
| 1394 | "objc2 0.5.2", | ||
| 1395 | "objc2-foundation 0.2.2", | ||
| 1396 | ] | ||
| 1397 | |||
| 855 | [[package]] | 1398 | [[package]] |
| 856 | name = "objc2-metal" | 1399 | name = "objc2-metal" |
| 857 | version = "0.3.2" | 1400 | version = "0.3.2" |
| 858 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 859 | checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" | 1402 | checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" |
| 860 | dependencies = [ | 1403 | dependencies = [ |
| 861 | "bitflags", | 1404 | "bitflags 2.13.1", |
| 862 | "block2", | 1405 | "block2 0.6.2", |
| 863 | "objc2", | 1406 | "objc2 0.6.4", |
| 864 | "objc2-foundation", | 1407 | "objc2-foundation 0.3.2", |
| 1408 | ] | ||
| 1409 | |||
| 1410 | [[package]] | ||
| 1411 | name = "objc2-quartz-core" | ||
| 1412 | version = "0.2.2" | ||
| 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1414 | checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" | ||
| 1415 | dependencies = [ | ||
| 1416 | "bitflags 2.13.1", | ||
| 1417 | "block2 0.5.1", | ||
| 1418 | "objc2 0.5.2", | ||
| 1419 | "objc2-foundation 0.2.2", | ||
| 1420 | "objc2-metal 0.2.2", | ||
| 865 | ] | 1421 | ] |
| 866 | 1422 | ||
| 867 | [[package]] | 1423 | [[package]] |
| ... | @@ -870,12 +1426,67 @@ version = "0.3.2" | ... | @@ -870,12 +1426,67 @@ version = "0.3.2" |
| 870 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 871 | checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" | 1427 | checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" |
| 872 | dependencies = [ | 1428 | dependencies = [ |
| 873 | "bitflags", | 1429 | "bitflags 2.13.1", |
| 874 | "objc2", | 1430 | "objc2 0.6.4", |
| 875 | "objc2-core-foundation", | 1431 | "objc2-core-foundation", |
| 876 | "objc2-core-graphics", | 1432 | "objc2-core-graphics", |
| 877 | "objc2-foundation", | 1433 | "objc2-foundation 0.3.2", |
| 878 | "objc2-metal", | 1434 | "objc2-metal 0.3.2", |
| 1435 | ] | ||
| 1436 | |||
| 1437 | [[package]] | ||
| 1438 | name = "objc2-symbols" | ||
| 1439 | version = "0.2.2" | ||
| 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1441 | checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" | ||
| 1442 | dependencies = [ | ||
| 1443 | "objc2 0.5.2", | ||
| 1444 | "objc2-foundation 0.2.2", | ||
| 1445 | ] | ||
| 1446 | |||
| 1447 | [[package]] | ||
| 1448 | name = "objc2-ui-kit" | ||
| 1449 | version = "0.2.2" | ||
| 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1451 | checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" | ||
| 1452 | dependencies = [ | ||
| 1453 | "bitflags 2.13.1", | ||
| 1454 | "block2 0.5.1", | ||
| 1455 | "objc2 0.5.2", | ||
| 1456 | "objc2-cloud-kit", | ||
| 1457 | "objc2-core-data", | ||
| 1458 | "objc2-core-image", | ||
| 1459 | "objc2-core-location", | ||
| 1460 | "objc2-foundation 0.2.2", | ||
| 1461 | "objc2-link-presentation", | ||
| 1462 | "objc2-quartz-core 0.2.2", | ||
| 1463 | "objc2-symbols", | ||
| 1464 | "objc2-uniform-type-identifiers", | ||
| 1465 | "objc2-user-notifications", | ||
| 1466 | ] | ||
| 1467 | |||
| 1468 | [[package]] | ||
| 1469 | name = "objc2-uniform-type-identifiers" | ||
| 1470 | version = "0.2.2" | ||
| 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1472 | checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" | ||
| 1473 | dependencies = [ | ||
| 1474 | "block2 0.5.1", | ||
| 1475 | "objc2 0.5.2", | ||
| 1476 | "objc2-foundation 0.2.2", | ||
| 1477 | ] | ||
| 1478 | |||
| 1479 | [[package]] | ||
| 1480 | name = "objc2-user-notifications" | ||
| 1481 | version = "0.2.2" | ||
| 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1483 | checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" | ||
| 1484 | dependencies = [ | ||
| 1485 | "bitflags 2.13.1", | ||
| 1486 | "block2 0.5.1", | ||
| 1487 | "objc2 0.5.2", | ||
| 1488 | "objc2-core-location", | ||
| 1489 | "objc2-foundation 0.2.2", | ||
| 879 | ] | 1490 | ] |
| 880 | 1491 | ||
| 881 | [[package]] | 1492 | [[package]] |
| ... | @@ -884,6 +1495,16 @@ version = "1.21.4" | ... | @@ -884,6 +1495,16 @@ version = "1.21.4" |
| 884 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 885 | checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" | 1496 | checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" |
| 886 | 1497 | ||
| 1498 | [[package]] | ||
| 1499 | name = "orbclient" | ||
| 1500 | version = "0.3.55" | ||
| 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1502 | checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" | ||
| 1503 | dependencies = [ | ||
| 1504 | "libc", | ||
| 1505 | "libredox", | ||
| 1506 | ] | ||
| 1507 | |||
| 887 | [[package]] | 1508 | [[package]] |
| 888 | name = "ordered-float" | 1509 | name = "ordered-float" |
| 889 | version = "5.5.0" | 1510 | version = "5.5.0" |
| ... | @@ -893,6 +1514,15 @@ dependencies = [ | ... | @@ -893,6 +1514,15 @@ dependencies = [ |
| 893 | "num-traits", | 1514 | "num-traits", |
| 894 | ] | 1515 | ] |
| 895 | 1516 | ||
| 1517 | [[package]] | ||
| 1518 | name = "owned_ttf_parser" | ||
| 1519 | version = "0.25.1" | ||
| 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1521 | checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" | ||
| 1522 | dependencies = [ | ||
| 1523 | "ttf-parser", | ||
| 1524 | ] | ||
| 1525 | |||
| 896 | [[package]] | 1526 | [[package]] |
| 897 | name = "parking_lot" | 1527 | name = "parking_lot" |
| 898 | version = "0.12.5" | 1528 | version = "0.12.5" |
| ... | @@ -911,7 +1541,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" | ... | @@ -911,7 +1541,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" |
| 911 | dependencies = [ | 1541 | dependencies = [ |
| 912 | "cfg-if", | 1542 | "cfg-if", |
| 913 | "libc", | 1543 | "libc", |
| 914 | "redox_syscall", | 1544 | "redox_syscall 0.5.18", |
| 915 | "smallvec", | 1545 | "smallvec", |
| 916 | "windows-link", | 1546 | "windows-link", |
| 917 | ] | 1547 | ] |
| ... | @@ -960,6 +1590,32 @@ dependencies = [ | ... | @@ -960,6 +1590,32 @@ dependencies = [ |
| 960 | "skrifa", | 1590 | "skrifa", |
| 961 | ] | 1591 | ] |
| 962 | 1592 | ||
| 1593 | [[package]] | ||
| 1594 | name = "percent-encoding" | ||
| 1595 | version = "2.3.2" | ||
| 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1597 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" | ||
| 1598 | |||
| 1599 | [[package]] | ||
| 1600 | name = "pin-project" | ||
| 1601 | version = "1.1.13" | ||
| 1602 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1603 | checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" | ||
| 1604 | dependencies = [ | ||
| 1605 | "pin-project-internal", | ||
| 1606 | ] | ||
| 1607 | |||
| 1608 | [[package]] | ||
| 1609 | name = "pin-project-internal" | ||
| 1610 | version = "1.1.13" | ||
| 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1612 | checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" | ||
| 1613 | dependencies = [ | ||
| 1614 | "proc-macro2", | ||
| 1615 | "quote", | ||
| 1616 | "syn 2.0.119", | ||
| 1617 | ] | ||
| 1618 | |||
| 963 | [[package]] | 1619 | [[package]] |
| 964 | name = "pin-project-lite" | 1620 | name = "pin-project-lite" |
| 965 | version = "0.2.17" | 1621 | version = "0.2.17" |
| ... | @@ -972,19 +1628,39 @@ version = "0.3.34" | ... | @@ -972,19 +1628,39 @@ version = "0.3.34" |
| 972 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 973 | checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" | 1629 | checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" |
| 974 | 1630 | ||
| 1631 | [[package]] | ||
| 1632 | name = "plain" | ||
| 1633 | version = "0.2.3" | ||
| 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1635 | checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" | ||
| 1636 | |||
| 975 | [[package]] | 1637 | [[package]] |
| 976 | name = "png" | 1638 | name = "png" |
| 977 | version = "0.18.1" | 1639 | version = "0.18.1" |
| 978 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 979 | checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" | 1641 | checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" |
| 980 | dependencies = [ | 1642 | dependencies = [ |
| 981 | "bitflags", | 1643 | "bitflags 2.13.1", |
| 982 | "crc32fast", | 1644 | "crc32fast", |
| 983 | "fdeflate", | 1645 | "fdeflate", |
| 984 | "flate2", | 1646 | "flate2", |
| 985 | "miniz_oxide 0.8.9", | 1647 | "miniz_oxide 0.8.9", |
| 986 | ] | 1648 | ] |
| 987 | 1649 | ||
| 1650 | [[package]] | ||
| 1651 | name = "polling" | ||
| 1652 | version = "3.11.0" | ||
| 1653 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1654 | checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" | ||
| 1655 | dependencies = [ | ||
| 1656 | "cfg-if", | ||
| 1657 | "concurrent-queue", | ||
| 1658 | "hermit-abi", | ||
| 1659 | "pin-project-lite", | ||
| 1660 | "rustix 1.1.5", | ||
| 1661 | "windows-sys 0.61.2", | ||
| 1662 | ] | ||
| 1663 | |||
| 988 | [[package]] | 1664 | [[package]] |
| 989 | name = "pollster" | 1665 | name = "pollster" |
| 990 | version = "0.4.0" | 1666 | version = "0.4.0" |
| ... | @@ -1027,11 +1703,21 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" | ... | @@ -1027,11 +1703,21 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" |
| 1027 | name = "probe" | 1703 | name = "probe" |
| 1028 | version = "0.0.0" | 1704 | version = "0.0.0" |
| 1029 | dependencies = [ | 1705 | dependencies = [ |
| 1030 | "gl21", | 1706 | "draw", |
| 1031 | "objc2", | 1707 | "objc2 0.6.4", |
| 1032 | "objc2-app-kit", | 1708 | "objc2-app-kit 0.3.2", |
| 1033 | "objc2-foundation", | 1709 | "objc2-foundation 0.3.2", |
| 1034 | "png", | 1710 | "png", |
| 1711 | "ui", | ||
| 1712 | ] | ||
| 1713 | |||
| 1714 | [[package]] | ||
| 1715 | name = "proc-macro-crate" | ||
| 1716 | version = "3.5.0" | ||
| 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1718 | checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" | ||
| 1719 | dependencies = [ | ||
| 1720 | "toml_edit", | ||
| 1035 | ] | 1721 | ] |
| 1036 | 1722 | ||
| 1037 | [[package]] | 1723 | [[package]] |
| ... | @@ -1061,6 +1747,15 @@ version = "2.0.1" | ... | @@ -1061,6 +1747,15 @@ version = "2.0.1" |
| 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1062 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" | 1748 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" |
| 1063 | 1749 | ||
| 1750 | [[package]] | ||
| 1751 | name = "quick-xml" | ||
| 1752 | version = "0.41.0" | ||
| 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1754 | checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" | ||
| 1755 | dependencies = [ | ||
| 1756 | "memchr", | ||
| 1757 | ] | ||
| 1758 | |||
| 1064 | [[package]] | 1759 | [[package]] |
| 1065 | name = "quote" | 1760 | name = "quote" |
| 1066 | version = "1.0.47" | 1761 | version = "1.0.47" |
| ... | @@ -1070,6 +1765,18 @@ dependencies = [ | ... | @@ -1070,6 +1765,18 @@ dependencies = [ |
| 1070 | "proc-macro2", | 1765 | "proc-macro2", |
| 1071 | ] | 1766 | ] |
| 1072 | 1767 | ||
| 1768 | [[package]] | ||
| 1769 | name = "r-efi" | ||
| 1770 | version = "5.3.0" | ||
| 1771 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1772 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | ||
| 1773 | |||
| 1774 | [[package]] | ||
| 1775 | name = "r-efi" | ||
| 1776 | version = "6.0.0" | ||
| 1777 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1778 | checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | ||
| 1779 | |||
| 1073 | [[package]] | 1780 | [[package]] |
| 1074 | name = "range-alloc" | 1781 | name = "range-alloc" |
| 1075 | version = "0.1.5" | 1782 | version = "0.1.5" |
| ... | @@ -1088,10 +1795,10 @@ version = "1.1.0" | ... | @@ -1088,10 +1795,10 @@ version = "1.1.0" |
| 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1089 | checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" | 1796 | checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" |
| 1090 | dependencies = [ | 1797 | dependencies = [ |
| 1091 | "objc2", | 1798 | "objc2 0.6.4", |
| 1092 | "objc2-core-foundation", | 1799 | "objc2-core-foundation", |
| 1093 | "objc2-foundation", | 1800 | "objc2-foundation 0.3.2", |
| 1094 | "objc2-quartz-core", | 1801 | "objc2-quartz-core 0.3.2", |
| 1095 | ] | 1802 | ] |
| 1096 | 1803 | ||
| 1097 | [[package]] | 1804 | [[package]] |
| ... | @@ -1105,13 +1812,31 @@ dependencies = [ | ... | @@ -1105,13 +1812,31 @@ dependencies = [ |
| 1105 | "once_cell", | 1812 | "once_cell", |
| 1106 | ] | 1813 | ] |
| 1107 | 1814 | ||
| 1815 | [[package]] | ||
| 1816 | name = "redox_syscall" | ||
| 1817 | version = "0.4.1" | ||
| 1818 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1819 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" | ||
| 1820 | dependencies = [ | ||
| 1821 | "bitflags 1.3.2", | ||
| 1822 | ] | ||
| 1823 | |||
| 1108 | [[package]] | 1824 | [[package]] |
| 1109 | name = "redox_syscall" | 1825 | name = "redox_syscall" |
| 1110 | version = "0.5.18" | 1826 | version = "0.5.18" |
| 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1827 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1112 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" | 1828 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" |
| 1113 | dependencies = [ | 1829 | dependencies = [ |
| 1114 | "bitflags", | 1830 | "bitflags 2.13.1", |
| 1831 | ] | ||
| 1832 | |||
| 1833 | [[package]] | ||
| 1834 | name = "redox_syscall" | ||
| 1835 | version = "0.9.4" | ||
| 1836 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1837 | checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" | ||
| 1838 | dependencies = [ | ||
| 1839 | "bitflags 2.13.1", | ||
| 1115 | ] | 1840 | ] |
| 1116 | 1841 | ||
| 1117 | [[package]] | 1842 | [[package]] |
| ... | @@ -1121,6 +1846,7 @@ dependencies = [ | ... | @@ -1121,6 +1846,7 @@ dependencies = [ |
| 1121 | "draw", | 1846 | "draw", |
| 1122 | "png", | 1847 | "png", |
| 1123 | "pollster", | 1848 | "pollster", |
| 1849 | "ui", | ||
| 1124 | "wgpu", | 1850 | "wgpu", |
| 1125 | ] | 1851 | ] |
| 1126 | 1852 | ||
| ... | @@ -1145,18 +1871,87 @@ version = "1.1.0" | ... | @@ -1145,18 +1871,87 @@ version = "1.1.0" |
| 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1146 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" | 1872 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" |
| 1147 | 1873 | ||
| 1874 | [[package]] | ||
| 1875 | name = "rustc_version" | ||
| 1876 | version = "0.4.1" | ||
| 1877 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1878 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" | ||
| 1879 | dependencies = [ | ||
| 1880 | "semver", | ||
| 1881 | ] | ||
| 1882 | |||
| 1883 | [[package]] | ||
| 1884 | name = "rustix" | ||
| 1885 | version = "0.38.44" | ||
| 1886 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1887 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" | ||
| 1888 | dependencies = [ | ||
| 1889 | "bitflags 2.13.1", | ||
| 1890 | "errno", | ||
| 1891 | "libc", | ||
| 1892 | "linux-raw-sys 0.4.15", | ||
| 1893 | "windows-sys 0.59.0", | ||
| 1894 | ] | ||
| 1895 | |||
| 1896 | [[package]] | ||
| 1897 | name = "rustix" | ||
| 1898 | version = "1.1.5" | ||
| 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1900 | checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" | ||
| 1901 | dependencies = [ | ||
| 1902 | "bitflags 2.13.1", | ||
| 1903 | "errno", | ||
| 1904 | "libc", | ||
| 1905 | "linux-raw-sys 0.12.1", | ||
| 1906 | "windows-sys 0.61.2", | ||
| 1907 | ] | ||
| 1908 | |||
| 1148 | [[package]] | 1909 | [[package]] |
| 1149 | name = "rustversion" | 1910 | name = "rustversion" |
| 1150 | version = "1.0.23" | 1911 | version = "1.0.23" |
| 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1152 | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" | 1913 | checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" |
| 1153 | 1914 | ||
| 1915 | [[package]] | ||
| 1916 | name = "same-file" | ||
| 1917 | version = "1.0.6" | ||
| 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1919 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" | ||
| 1920 | dependencies = [ | ||
| 1921 | "winapi-util", | ||
| 1922 | ] | ||
| 1923 | |||
| 1924 | [[package]] | ||
| 1925 | name = "scoped-tls" | ||
| 1926 | version = "1.0.1" | ||
| 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1928 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" | ||
| 1929 | |||
| 1154 | [[package]] | 1930 | [[package]] |
| 1155 | name = "scopeguard" | 1931 | name = "scopeguard" |
| 1156 | version = "1.2.0" | 1932 | version = "1.2.0" |
| 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1933 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1158 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | 1934 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" |
| 1159 | 1935 | ||
| 1936 | [[package]] | ||
| 1937 | name = "sctk-adwaita" | ||
| 1938 | version = "0.10.1" | ||
| 1939 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1940 | checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" | ||
| 1941 | dependencies = [ | ||
| 1942 | "ab_glyph", | ||
| 1943 | "log", | ||
| 1944 | "memmap2", | ||
| 1945 | "smithay-client-toolkit", | ||
| 1946 | "tiny-skia", | ||
| 1947 | ] | ||
| 1948 | |||
| 1949 | [[package]] | ||
| 1950 | name = "semver" | ||
| 1951 | version = "1.0.28" | ||
| 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1953 | checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" | ||
| 1954 | |||
| 1160 | [[package]] | 1955 | [[package]] |
| 1161 | name = "serde" | 1956 | name = "serde" |
| 1162 | version = "1.0.229" | 1957 | version = "1.0.229" |
| ... | @@ -1187,12 +1982,34 @@ dependencies = [ | ... | @@ -1187,12 +1982,34 @@ dependencies = [ |
| 1187 | "syn 3.0.6", | 1982 | "syn 3.0.6", |
| 1188 | ] | 1983 | ] |
| 1189 | 1984 | ||
| 1985 | [[package]] | ||
| 1986 | name = "shlex" | ||
| 1987 | version = "2.0.1" | ||
| 1988 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1989 | checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | ||
| 1990 | |||
| 1190 | [[package]] | 1991 | [[package]] |
| 1191 | name = "simd-adler32" | 1992 | name = "simd-adler32" |
| 1192 | version = "0.3.10" | 1993 | version = "0.3.10" |
| 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1994 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1194 | checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" | 1995 | checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" |
| 1195 | 1996 | ||
| 1997 | [[package]] | ||
| 1998 | name = "simd_cesu8" | ||
| 1999 | version = "1.2.0" | ||
| 2000 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2001 | checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" | ||
| 2002 | dependencies = [ | ||
| 2003 | "rustc_version", | ||
| 2004 | "simdutf8", | ||
| 2005 | ] | ||
| 2006 | |||
| 2007 | [[package]] | ||
| 2008 | name = "simdutf8" | ||
| 2009 | version = "0.1.5" | ||
| 2010 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2011 | checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" | ||
| 2012 | |||
| 1196 | [[package]] | 2013 | [[package]] |
| 1197 | name = "skrifa" | 2014 | name = "skrifa" |
| 1198 | version = "0.44.0" | 2015 | version = "0.44.0" |
| ... | @@ -1224,13 +2041,47 @@ version = "1.16.2" | ... | @@ -1224,13 +2041,47 @@ version = "1.16.2" |
| 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2041 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1225 | checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" | 2042 | checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" |
| 1226 | 2043 | ||
| 2044 | [[package]] | ||
| 2045 | name = "smithay-client-toolkit" | ||
| 2046 | version = "0.19.2" | ||
| 2047 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2048 | checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" | ||
| 2049 | dependencies = [ | ||
| 2050 | "bitflags 2.13.1", | ||
| 2051 | "calloop", | ||
| 2052 | "calloop-wayland-source", | ||
| 2053 | "cursor-icon", | ||
| 2054 | "libc", | ||
| 2055 | "log", | ||
| 2056 | "memmap2", | ||
| 2057 | "rustix 0.38.44", | ||
| 2058 | "thiserror 1.0.69", | ||
| 2059 | "wayland-backend", | ||
| 2060 | "wayland-client", | ||
| 2061 | "wayland-csd-frame", | ||
| 2062 | "wayland-cursor", | ||
| 2063 | "wayland-protocols", | ||
| 2064 | "wayland-protocols-wlr", | ||
| 2065 | "wayland-scanner", | ||
| 2066 | "xkeysym", | ||
| 2067 | ] | ||
| 2068 | |||
| 2069 | [[package]] | ||
| 2070 | name = "smol_str" | ||
| 2071 | version = "0.2.2" | ||
| 2072 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2073 | checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" | ||
| 2074 | dependencies = [ | ||
| 2075 | "serde", | ||
| 2076 | ] | ||
| 2077 | |||
| 1227 | [[package]] | 2078 | [[package]] |
| 1228 | name = "spirv" | 2079 | name = "spirv" |
| 1229 | version = "0.4.0+sdk-1.4.341.0" | 2080 | version = "0.4.0+sdk-1.4.341.0" |
| 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2081 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1231 | checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" | 2082 | checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" |
| 1232 | dependencies = [ | 2083 | dependencies = [ |
| 1233 | "bitflags", | 2084 | "bitflags 2.13.1", |
| 1234 | ] | 2085 | ] |
| 1235 | 2086 | ||
| 1236 | [[package]] | 2087 | [[package]] |
| ... | @@ -1245,6 +2096,12 @@ version = "1.1.0" | ... | @@ -1245,6 +2096,12 @@ version = "1.1.0" |
| 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2096 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1246 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" | 2097 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" |
| 1247 | 2098 | ||
| 2099 | [[package]] | ||
| 2100 | name = "strict-num" | ||
| 2101 | version = "0.1.1" | ||
| 2102 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2103 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" | ||
| 2104 | |||
| 1248 | [[package]] | 2105 | [[package]] |
| 1249 | name = "swash" | 2106 | name = "swash" |
| 1250 | version = "0.2.10" | 2107 | version = "0.2.10" |
| ... | @@ -1298,13 +2155,33 @@ dependencies = [ | ... | @@ -1298,13 +2155,33 @@ dependencies = [ |
| 1298 | "winapi-util", | 2155 | "winapi-util", |
| 1299 | ] | 2156 | ] |
| 1300 | 2157 | ||
| 2158 | [[package]] | ||
| 2159 | name = "thiserror" | ||
| 2160 | version = "1.0.69" | ||
| 2161 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2162 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" | ||
| 2163 | dependencies = [ | ||
| 2164 | "thiserror-impl 1.0.69", | ||
| 2165 | ] | ||
| 2166 | |||
| 1301 | [[package]] | 2167 | [[package]] |
| 1302 | name = "thiserror" | 2168 | name = "thiserror" |
| 1303 | version = "2.0.21" | 2169 | version = "2.0.21" |
| 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2170 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1305 | checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" | 2171 | checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" |
| 1306 | dependencies = [ | 2172 | dependencies = [ |
| 1307 | "thiserror-impl", | 2173 | "thiserror-impl 2.0.21", |
| 2174 | ] | ||
| 2175 | |||
| 2176 | [[package]] | ||
| 2177 | name = "thiserror-impl" | ||
| 2178 | version = "1.0.69" | ||
| 2179 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2180 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | ||
| 2181 | dependencies = [ | ||
| 2182 | "proc-macro2", | ||
| 2183 | "quote", | ||
| 2184 | "syn 2.0.119", | ||
| 1308 | ] | 2185 | ] |
| 1309 | 2186 | ||
| 1310 | [[package]] | 2187 | [[package]] |
| ... | @@ -1332,6 +2209,31 @@ dependencies = [ | ... | @@ -1332,6 +2209,31 @@ dependencies = [ |
| 1332 | "zune-jpeg", | 2209 | "zune-jpeg", |
| 1333 | ] | 2210 | ] |
| 1334 | 2211 | ||
| 2212 | [[package]] | ||
| 2213 | name = "tiny-skia" | ||
| 2214 | version = "0.11.4" | ||
| 2215 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2216 | checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" | ||
| 2217 | dependencies = [ | ||
| 2218 | "arrayref", | ||
| 2219 | "arrayvec", | ||
| 2220 | "bytemuck", | ||
| 2221 | "cfg-if", | ||
| 2222 | "log", | ||
| 2223 | "tiny-skia-path", | ||
| 2224 | ] | ||
| 2225 | |||
| 2226 | [[package]] | ||
| 2227 | name = "tiny-skia-path" | ||
| 2228 | version = "0.11.4" | ||
| 2229 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2230 | checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" | ||
| 2231 | dependencies = [ | ||
| 2232 | "arrayref", | ||
| 2233 | "bytemuck", | ||
| 2234 | "strict-num", | ||
| 2235 | ] | ||
| 2236 | |||
| 1335 | [[package]] | 2237 | [[package]] |
| 1336 | name = "tinystr" | 2238 | name = "tinystr" |
| 1337 | version = "0.8.4" | 2239 | version = "0.8.4" |
| ... | @@ -1343,12 +2245,80 @@ dependencies = [ | ... | @@ -1343,12 +2245,80 @@ dependencies = [ |
| 1343 | "zerovec", | 2245 | "zerovec", |
| 1344 | ] | 2246 | ] |
| 1345 | 2247 | ||
| 2248 | [[package]] | ||
| 2249 | name = "toml_datetime" | ||
| 2250 | version = "1.1.1+spec-1.1.0" | ||
| 2251 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2252 | checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" | ||
| 2253 | dependencies = [ | ||
| 2254 | "serde_core", | ||
| 2255 | ] | ||
| 2256 | |||
| 2257 | [[package]] | ||
| 2258 | name = "toml_edit" | ||
| 2259 | version = "0.25.15+spec-1.1.0" | ||
| 2260 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2261 | checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" | ||
| 2262 | dependencies = [ | ||
| 2263 | "indexmap", | ||
| 2264 | "toml_datetime", | ||
| 2265 | "toml_parser", | ||
| 2266 | "winnow", | ||
| 2267 | ] | ||
| 2268 | |||
| 2269 | [[package]] | ||
| 2270 | name = "toml_parser" | ||
| 2271 | version = "1.1.3+spec-1.1.0" | ||
| 2272 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2273 | checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" | ||
| 2274 | dependencies = [ | ||
| 2275 | "winnow", | ||
| 2276 | ] | ||
| 2277 | |||
| 2278 | [[package]] | ||
| 2279 | name = "tracing" | ||
| 2280 | version = "0.1.44" | ||
| 2281 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2282 | checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | ||
| 2283 | dependencies = [ | ||
| 2284 | "pin-project-lite", | ||
| 2285 | "tracing-core", | ||
| 2286 | ] | ||
| 2287 | |||
| 2288 | [[package]] | ||
| 2289 | name = "tracing-core" | ||
| 2290 | version = "0.1.36" | ||
| 2291 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2292 | checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" | ||
| 2293 | |||
| 2294 | [[package]] | ||
| 2295 | name = "ttf-parser" | ||
| 2296 | version = "0.25.1" | ||
| 2297 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2298 | checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" | ||
| 2299 | |||
| 2300 | [[package]] | ||
| 2301 | name = "ui" | ||
| 2302 | version = "0.1.0" | ||
| 2303 | dependencies = [ | ||
| 2304 | "draw", | ||
| 2305 | "nucleo-matcher", | ||
| 2306 | "parley", | ||
| 2307 | "winit", | ||
| 2308 | ] | ||
| 2309 | |||
| 1346 | [[package]] | 2310 | [[package]] |
| 1347 | name = "unicode-ident" | 2311 | name = "unicode-ident" |
| 1348 | version = "1.0.26" | 2312 | version = "1.0.26" |
| 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2313 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1350 | checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" | 2314 | checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" |
| 1351 | 2315 | ||
| 2316 | [[package]] | ||
| 2317 | name = "unicode-segmentation" | ||
| 2318 | version = "1.13.3" | ||
| 2319 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2320 | checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" | ||
| 2321 | |||
| 1352 | [[package]] | 2322 | [[package]] |
| 1353 | name = "unicode-width" | 2323 | name = "unicode-width" |
| 1354 | version = "0.2.2" | 2324 | version = "0.2.2" |
| ... | @@ -1367,6 +2337,25 @@ version = "0.9.5" | ... | @@ -1367,6 +2337,25 @@ version = "0.9.5" |
| 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2337 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1368 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | 2338 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" |
| 1369 | 2339 | ||
| 2340 | [[package]] | ||
| 2341 | name = "walkdir" | ||
| 2342 | version = "2.5.0" | ||
| 2343 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2344 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" | ||
| 2345 | dependencies = [ | ||
| 2346 | "same-file", | ||
| 2347 | "winapi-util", | ||
| 2348 | ] | ||
| 2349 | |||
| 2350 | [[package]] | ||
| 2351 | name = "wasip2" | ||
| 2352 | version = "1.0.4+wasi-0.2.12" | ||
| 2353 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2354 | checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" | ||
| 2355 | dependencies = [ | ||
| 2356 | "wit-bindgen", | ||
| 2357 | ] | ||
| 2358 | |||
| 1370 | [[package]] | 2359 | [[package]] |
| 1371 | name = "wasm-bindgen" | 2360 | name = "wasm-bindgen" |
| 1372 | version = "0.2.128" | 2361 | version = "0.2.128" |
| ... | @@ -1422,6 +2411,103 @@ dependencies = [ | ... | @@ -1422,6 +2411,103 @@ dependencies = [ |
| 1422 | "unicode-ident", | 2411 | "unicode-ident", |
| 1423 | ] | 2412 | ] |
| 1424 | 2413 | ||
| 2414 | [[package]] | ||
| 2415 | name = "wayland-backend" | ||
| 2416 | version = "0.3.17" | ||
| 2417 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2418 | checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" | ||
| 2419 | dependencies = [ | ||
| 2420 | "cc", | ||
| 2421 | "downcast-rs", | ||
| 2422 | "rustix 1.1.5", | ||
| 2423 | "scoped-tls", | ||
| 2424 | "smallvec", | ||
| 2425 | "wayland-sys", | ||
| 2426 | ] | ||
| 2427 | |||
| 2428 | [[package]] | ||
| 2429 | name = "wayland-client" | ||
| 2430 | version = "0.31.15" | ||
| 2431 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2432 | checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" | ||
| 2433 | dependencies = [ | ||
| 2434 | "bitflags 2.13.1", | ||
| 2435 | "rustix 1.1.5", | ||
| 2436 | "wayland-backend", | ||
| 2437 | "wayland-scanner", | ||
| 2438 | ] | ||
| 2439 | |||
| 2440 | [[package]] | ||
| 2441 | name = "wayland-csd-frame" | ||
| 2442 | version = "0.3.0" | ||
| 2443 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2444 | checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" | ||
| 2445 | dependencies = [ | ||
| 2446 | "bitflags 2.13.1", | ||
| 2447 | "cursor-icon", | ||
| 2448 | "wayland-backend", | ||
| 2449 | ] | ||
| 2450 | |||
| 2451 | [[package]] | ||
| 2452 | name = "wayland-cursor" | ||
| 2453 | version = "0.31.14" | ||
| 2454 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2455 | checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" | ||
| 2456 | dependencies = [ | ||
| 2457 | "rustix 1.1.5", | ||
| 2458 | "wayland-client", | ||
| 2459 | "xcursor", | ||
| 2460 | ] | ||
| 2461 | |||
| 2462 | [[package]] | ||
| 2463 | name = "wayland-protocols" | ||
| 2464 | version = "0.32.13" | ||
| 2465 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2466 | checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" | ||
| 2467 | dependencies = [ | ||
| 2468 | "bitflags 2.13.1", | ||
| 2469 | "wayland-backend", | ||
| 2470 | "wayland-client", | ||
| 2471 | "wayland-scanner", | ||
| 2472 | ] | ||
| 2473 | |||
| 2474 | [[package]] | ||
| 2475 | name = "wayland-protocols-plasma" | ||
| 2476 | version = "0.3.12" | ||
| 2477 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2478 | checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" | ||
| 2479 | dependencies = [ | ||
| 2480 | "bitflags 2.13.1", | ||
| 2481 | "wayland-backend", | ||
| 2482 | "wayland-client", | ||
| 2483 | "wayland-protocols", | ||
| 2484 | "wayland-scanner", | ||
| 2485 | ] | ||
| 2486 | |||
| 2487 | [[package]] | ||
| 2488 | name = "wayland-protocols-wlr" | ||
| 2489 | version = "0.3.12" | ||
| 2490 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2491 | checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" | ||
| 2492 | dependencies = [ | ||
| 2493 | "bitflags 2.13.1", | ||
| 2494 | "wayland-backend", | ||
| 2495 | "wayland-client", | ||
| 2496 | "wayland-protocols", | ||
| 2497 | "wayland-scanner", | ||
| 2498 | ] | ||
| 2499 | |||
| 2500 | [[package]] | ||
| 2501 | name = "wayland-scanner" | ||
| 2502 | version = "0.31.11" | ||
| 2503 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2504 | checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" | ||
| 2505 | dependencies = [ | ||
| 2506 | "proc-macro2", | ||
| 2507 | "quick-xml", | ||
| 2508 | "quote", | ||
| 2509 | ] | ||
| 2510 | |||
| 1425 | [[package]] | 2511 | [[package]] |
| 1426 | name = "wayland-sys" | 2512 | name = "wayland-sys" |
| 1427 | version = "0.31.11" | 2513 | version = "0.31.11" |
| ... | @@ -1444,6 +2530,16 @@ dependencies = [ | ... | @@ -1444,6 +2530,16 @@ dependencies = [ |
| 1444 | "wasm-bindgen", | 2530 | "wasm-bindgen", |
| 1445 | ] | 2531 | ] |
| 1446 | 2532 | ||
| 2533 | [[package]] | ||
| 2534 | name = "web-time" | ||
| 2535 | version = "1.1.0" | ||
| 2536 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2537 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" | ||
| 2538 | dependencies = [ | ||
| 2539 | "js-sys", | ||
| 2540 | "wasm-bindgen", | ||
| 2541 | ] | ||
| 2542 | |||
| 1447 | [[package]] | 2543 | [[package]] |
| 1448 | name = "weezl" | 2544 | name = "weezl" |
| 1449 | version = "0.1.12" | 2545 | version = "0.1.12" |
| ... | @@ -1457,7 +2553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ... | @@ -1457,7 +2553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1457 | checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" | 2553 | checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" |
| 1458 | dependencies = [ | 2554 | dependencies = [ |
| 1459 | "arrayvec", | 2555 | "arrayvec", |
| 1460 | "bitflags", | 2556 | "bitflags 2.13.1", |
| 1461 | "bytemuck", | 2557 | "bytemuck", |
| 1462 | "cfg-if", | 2558 | "cfg-if", |
| 1463 | "cfg_aliases", | 2559 | "cfg_aliases", |
| ... | @@ -1489,7 +2585,7 @@ dependencies = [ | ... | @@ -1489,7 +2585,7 @@ dependencies = [ |
| 1489 | "arrayvec", | 2585 | "arrayvec", |
| 1490 | "bit-set", | 2586 | "bit-set", |
| 1491 | "bit-vec", | 2587 | "bit-vec", |
| 1492 | "bitflags", | 2588 | "bitflags 2.13.1", |
| 1493 | "bytemuck", | 2589 | "bytemuck", |
| 1494 | "cfg_aliases", | 2590 | "cfg_aliases", |
| 1495 | "document-features", | 2591 | "document-features", |
| ... | @@ -1505,7 +2601,7 @@ dependencies = [ | ... | @@ -1505,7 +2601,7 @@ dependencies = [ |
| 1505 | "raw-window-handle", | 2601 | "raw-window-handle", |
| 1506 | "rustc-hash", | 2602 | "rustc-hash", |
| 1507 | "smallvec", | 2603 | "smallvec", |
| 1508 | "thiserror", | 2604 | "thiserror 2.0.21", |
| 1509 | "wgpu-core-deps-apple", | 2605 | "wgpu-core-deps-apple", |
| 1510 | "wgpu-core-deps-emscripten", | 2606 | "wgpu-core-deps-emscripten", |
| 1511 | "wgpu-core-deps-windows-linux-android", | 2607 | "wgpu-core-deps-windows-linux-android", |
| ... | @@ -1551,8 +2647,8 @@ dependencies = [ | ... | @@ -1551,8 +2647,8 @@ dependencies = [ |
| 1551 | "arrayvec", | 2647 | "arrayvec", |
| 1552 | "ash", | 2648 | "ash", |
| 1553 | "bit-set", | 2649 | "bit-set", |
| 1554 | "bitflags", | 2650 | "bitflags 2.13.1", |
| 1555 | "block2", | 2651 | "block2 0.6.2", |
| 1556 | "bytemuck", | 2652 | "bytemuck", |
| 1557 | "cfg-if", | 2653 | "cfg-if", |
| 1558 | "cfg_aliases", | 2654 | "cfg_aliases", |
| ... | @@ -1568,12 +2664,12 @@ dependencies = [ | ... | @@ -1568,12 +2664,12 @@ dependencies = [ |
| 1568 | "naga", | 2664 | "naga", |
| 1569 | "naga-types", | 2665 | "naga-types", |
| 1570 | "ndk-sys", | 2666 | "ndk-sys", |
| 1571 | "objc2", | 2667 | "objc2 0.6.4", |
| 1572 | "objc2-core-foundation", | 2668 | "objc2-core-foundation", |
| 1573 | "objc2-core-graphics", | 2669 | "objc2-core-graphics", |
| 1574 | "objc2-foundation", | 2670 | "objc2-foundation 0.3.2", |
| 1575 | "objc2-metal", | 2671 | "objc2-metal 0.3.2", |
| 1576 | "objc2-quartz-core", | 2672 | "objc2-quartz-core 0.3.2", |
| 1577 | "once_cell", | 2673 | "once_cell", |
| 1578 | "ordered-float", | 2674 | "ordered-float", |
| 1579 | "parking_lot", | 2675 | "parking_lot", |
| ... | @@ -1586,7 +2682,7 @@ dependencies = [ | ... | @@ -1586,7 +2682,7 @@ dependencies = [ |
| 1586 | "renderdoc-sys", | 2682 | "renderdoc-sys", |
| 1587 | "smallvec", | 2683 | "smallvec", |
| 1588 | "static_assertions", | 2684 | "static_assertions", |
| 1589 | "thiserror", | 2685 | "thiserror 2.0.21", |
| 1590 | "wasm-bindgen", | 2686 | "wasm-bindgen", |
| 1591 | "wayland-sys", | 2687 | "wayland-sys", |
| 1592 | "web-sys", | 2688 | "web-sys", |
| ... | @@ -1613,7 +2709,7 @@ version = "30.0.1" | ... | @@ -1613,7 +2709,7 @@ version = "30.0.1" |
| 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2709 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1614 | checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" | 2710 | checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" |
| 1615 | dependencies = [ | 2711 | dependencies = [ |
| 1616 | "bitflags", | 2712 | "bitflags 2.13.1", |
| 1617 | "bytemuck", | 2713 | "bytemuck", |
| 1618 | "js-sys", | 2714 | "js-sys", |
| 1619 | "log", | 2715 | "log", |
| ... | @@ -1629,7 +2725,7 @@ version = "0.1.11" | ... | @@ -1629,7 +2725,7 @@ version = "0.1.11" |
| 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2725 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1630 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | 2726 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" |
| 1631 | dependencies = [ | 2727 | dependencies = [ |
| 1632 | "windows-sys", | 2728 | "windows-sys 0.61.2", |
| 1633 | ] | 2729 | ] |
| 1634 | 2730 | ||
| 1635 | [[package]] | 2731 | [[package]] |
| ... | @@ -1733,6 +2829,24 @@ dependencies = [ | ... | @@ -1733,6 +2829,24 @@ dependencies = [ |
| 1733 | "windows-link", | 2829 | "windows-link", |
| 1734 | ] | 2830 | ] |
| 1735 | 2831 | ||
| 2832 | [[package]] | ||
| 2833 | name = "windows-sys" | ||
| 2834 | version = "0.52.0" | ||
| 2835 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2836 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" | ||
| 2837 | dependencies = [ | ||
| 2838 | "windows-targets", | ||
| 2839 | ] | ||
| 2840 | |||
| 2841 | [[package]] | ||
| 2842 | name = "windows-sys" | ||
| 2843 | version = "0.59.0" | ||
| 2844 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2845 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" | ||
| 2846 | dependencies = [ | ||
| 2847 | "windows-targets", | ||
| 2848 | ] | ||
| 2849 | |||
| 1736 | [[package]] | 2850 | [[package]] |
| 1737 | name = "windows-sys" | 2851 | name = "windows-sys" |
| 1738 | version = "0.61.2" | 2852 | version = "0.61.2" |
| ... | @@ -1742,6 +2856,22 @@ dependencies = [ | ... | @@ -1742,6 +2856,22 @@ dependencies = [ |
| 1742 | "windows-link", | 2856 | "windows-link", |
| 1743 | ] | 2857 | ] |
| 1744 | 2858 | ||
| 2859 | [[package]] | ||
| 2860 | name = "windows-targets" | ||
| 2861 | version = "0.52.6" | ||
| 2862 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2863 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" | ||
| 2864 | dependencies = [ | ||
| 2865 | "windows_aarch64_gnullvm", | ||
| 2866 | "windows_aarch64_msvc", | ||
| 2867 | "windows_i686_gnu", | ||
| 2868 | "windows_i686_gnullvm", | ||
| 2869 | "windows_i686_msvc", | ||
| 2870 | "windows_x86_64_gnu", | ||
| 2871 | "windows_x86_64_gnullvm", | ||
| 2872 | "windows_x86_64_msvc", | ||
| 2873 | ] | ||
| 2874 | |||
| 1745 | [[package]] | 2875 | [[package]] |
| 1746 | name = "windows-threading" | 2876 | name = "windows-threading" |
| 1747 | version = "0.2.1" | 2877 | version = "0.2.1" |
| ... | @@ -1751,12 +2881,184 @@ dependencies = [ | ... | @@ -1751,12 +2881,184 @@ dependencies = [ |
| 1751 | "windows-link", | 2881 | "windows-link", |
| 1752 | ] | 2882 | ] |
| 1753 | 2883 | ||
| 2884 | [[package]] | ||
| 2885 | name = "windows_aarch64_gnullvm" | ||
| 2886 | version = "0.52.6" | ||
| 2887 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2888 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" | ||
| 2889 | |||
| 2890 | [[package]] | ||
| 2891 | name = "windows_aarch64_msvc" | ||
| 2892 | version = "0.52.6" | ||
| 2893 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2894 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" | ||
| 2895 | |||
| 2896 | [[package]] | ||
| 2897 | name = "windows_i686_gnu" | ||
| 2898 | version = "0.52.6" | ||
| 2899 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2900 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" | ||
| 2901 | |||
| 2902 | [[package]] | ||
| 2903 | name = "windows_i686_gnullvm" | ||
| 2904 | version = "0.52.6" | ||
| 2905 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2906 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" | ||
| 2907 | |||
| 2908 | [[package]] | ||
| 2909 | name = "windows_i686_msvc" | ||
| 2910 | version = "0.52.6" | ||
| 2911 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2912 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" | ||
| 2913 | |||
| 2914 | [[package]] | ||
| 2915 | name = "windows_x86_64_gnu" | ||
| 2916 | version = "0.52.6" | ||
| 2917 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2918 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" | ||
| 2919 | |||
| 2920 | [[package]] | ||
| 2921 | name = "windows_x86_64_gnullvm" | ||
| 2922 | version = "0.52.6" | ||
| 2923 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2924 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" | ||
| 2925 | |||
| 2926 | [[package]] | ||
| 2927 | name = "windows_x86_64_msvc" | ||
| 2928 | version = "0.52.6" | ||
| 2929 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2930 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | ||
| 2931 | |||
| 2932 | [[package]] | ||
| 2933 | name = "winit" | ||
| 2934 | version = "0.30.13" | ||
| 2935 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2936 | checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" | ||
| 2937 | dependencies = [ | ||
| 2938 | "ahash", | ||
| 2939 | "android-activity", | ||
| 2940 | "atomic-waker", | ||
| 2941 | "bitflags 2.13.1", | ||
| 2942 | "block2 0.5.1", | ||
| 2943 | "bytemuck", | ||
| 2944 | "calloop", | ||
| 2945 | "cfg_aliases", | ||
| 2946 | "concurrent-queue", | ||
| 2947 | "core-foundation", | ||
| 2948 | "core-graphics", | ||
| 2949 | "cursor-icon", | ||
| 2950 | "dpi", | ||
| 2951 | "js-sys", | ||
| 2952 | "libc", | ||
| 2953 | "memmap2", | ||
| 2954 | "ndk", | ||
| 2955 | "objc2 0.5.2", | ||
| 2956 | "objc2-app-kit 0.2.2", | ||
| 2957 | "objc2-foundation 0.2.2", | ||
| 2958 | "objc2-ui-kit", | ||
| 2959 | "orbclient", | ||
| 2960 | "percent-encoding", | ||
| 2961 | "pin-project", | ||
| 2962 | "raw-window-handle", | ||
| 2963 | "redox_syscall 0.4.1", | ||
| 2964 | "rustix 0.38.44", | ||
| 2965 | "sctk-adwaita", | ||
| 2966 | "smithay-client-toolkit", | ||
| 2967 | "smol_str", | ||
| 2968 | "tracing", | ||
| 2969 | "unicode-segmentation", | ||
| 2970 | "wasm-bindgen", | ||
| 2971 | "wasm-bindgen-futures", | ||
| 2972 | "wayland-backend", | ||
| 2973 | "wayland-client", | ||
| 2974 | "wayland-protocols", | ||
| 2975 | "wayland-protocols-plasma", | ||
| 2976 | "web-sys", | ||
| 2977 | "web-time", | ||
| 2978 | "windows-sys 0.52.0", | ||
| 2979 | "x11-dl", | ||
| 2980 | "x11rb", | ||
| 2981 | "xkbcommon-dl", | ||
| 2982 | ] | ||
| 2983 | |||
| 2984 | [[package]] | ||
| 2985 | name = "winnow" | ||
| 2986 | version = "1.0.4" | ||
| 2987 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2988 | checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" | ||
| 2989 | dependencies = [ | ||
| 2990 | "memchr", | ||
| 2991 | ] | ||
| 2992 | |||
| 2993 | [[package]] | ||
| 2994 | name = "wit-bindgen" | ||
| 2995 | version = "0.57.1" | ||
| 2996 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2997 | checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" | ||
| 2998 | |||
| 1754 | [[package]] | 2999 | [[package]] |
| 1755 | name = "writeable" | 3000 | name = "writeable" |
| 1756 | version = "0.6.4" | 3001 | version = "0.6.4" |
| 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3002 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1758 | checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" | 3003 | checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" |
| 1759 | 3004 | ||
| 3005 | [[package]] | ||
| 3006 | name = "x11-dl" | ||
| 3007 | version = "2.21.0" | ||
| 3008 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3009 | checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" | ||
| 3010 | dependencies = [ | ||
| 3011 | "libc", | ||
| 3012 | "once_cell", | ||
| 3013 | "pkg-config", | ||
| 3014 | ] | ||
| 3015 | |||
| 3016 | [[package]] | ||
| 3017 | name = "x11rb" | ||
| 3018 | version = "0.13.2" | ||
| 3019 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3020 | checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" | ||
| 3021 | dependencies = [ | ||
| 3022 | "as-raw-xcb-connection", | ||
| 3023 | "gethostname", | ||
| 3024 | "libc", | ||
| 3025 | "libloading", | ||
| 3026 | "once_cell", | ||
| 3027 | "rustix 1.1.5", | ||
| 3028 | "x11rb-protocol", | ||
| 3029 | ] | ||
| 3030 | |||
| 3031 | [[package]] | ||
| 3032 | name = "x11rb-protocol" | ||
| 3033 | version = "0.13.2" | ||
| 3034 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3035 | checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" | ||
| 3036 | |||
| 3037 | [[package]] | ||
| 3038 | name = "xcursor" | ||
| 3039 | version = "0.3.11" | ||
| 3040 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3041 | checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" | ||
| 3042 | |||
| 3043 | [[package]] | ||
| 3044 | name = "xkbcommon-dl" | ||
| 3045 | version = "0.4.2" | ||
| 3046 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3047 | checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" | ||
| 3048 | dependencies = [ | ||
| 3049 | "bitflags 2.13.1", | ||
| 3050 | "dlib", | ||
| 3051 | "log", | ||
| 3052 | "once_cell", | ||
| 3053 | "xkeysym", | ||
| 3054 | ] | ||
| 3055 | |||
| 3056 | [[package]] | ||
| 3057 | name = "xkeysym" | ||
| 3058 | version = "0.2.1" | ||
| 3059 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3060 | checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" | ||
| 3061 | |||
| 1760 | [[package]] | 3062 | [[package]] |
| 1761 | name = "xml-rs" | 3063 | name = "xml-rs" |
| 1762 | version = "0.8.29" | 3064 | version = "0.8.29" |
platform/snow-leopard/Cargo.toml+1-1| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | # Mac OS X 10.6 work, built with cargo.sh. Its own workspace so the 10.6 target | 1 | # Mac OS X 10.6 work, built with cargo.sh. Its own workspace so the 10.6 target |
| 2 | # never enters the main workspace's builds. | 2 | # never enters the main workspace's builds. |
| 3 | [workspace] | 3 | [workspace] |
| 4 | members = ["gl21", "hello", "probe", "reference"] | 4 | members = ["hello", "probe", "reference"] |
| 5 | resolver = "3" | 5 | resolver = "3" |
| 6 | 6 | ||
| 7 | [workspace.dependencies] | 7 | [workspace.dependencies] |
platform/snow-leopard/console/input.c created+48| ... | @@ -0,0 +1,48 @@ | ||
| 1 | // Posts input to the logged-in session as the HID system would, for driving the app | ||
| 2 | // over ssh: `input click X Y`, `input type TEXT`, `input key KEYCODE [command]`. | ||
| 3 | // Points are the display's, from its top left. | ||
| 4 | #include <ApplicationServices/ApplicationServices.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | #include <string.h> | ||
| 7 | #include <unistd.h> | ||
| 8 | |||
| 9 | static void post(CGEventRef event) { | ||
| 10 | CGEventPost(kCGHIDEventTap, event); | ||
| 11 | CFRelease(event); | ||
| 12 | usleep(20000); | ||
| 13 | } | ||
| 14 | |||
| 15 | static void mouse(CGEventType type, CGPoint at) { | ||
| 16 | post(CGEventCreateMouseEvent(NULL, type, at, kCGMouseButtonLeft)); | ||
| 17 | } | ||
| 18 | |||
| 19 | static void key(CGKeyCode code, CGEventFlags flags, const UniChar *text, UniCharCount length) { | ||
| 20 | for (int down = 1; down >= 0; down--) { | ||
| 21 | CGEventRef event = CGEventCreateKeyboardEvent(NULL, code, down); | ||
| 22 | CGEventSetFlags(event, flags); | ||
| 23 | if (length) CGEventKeyboardSetUnicodeString(event, length, text); | ||
| 24 | post(event); | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | int main(int argc, char **argv) { | ||
| 29 | if (argc == 4 && !strcmp(argv[1], "click")) { | ||
| 30 | CGPoint at = CGPointMake(atof(argv[2]), atof(argv[3])); | ||
| 31 | mouse(kCGEventMouseMoved, at); | ||
| 32 | mouse(kCGEventLeftMouseDown, at); | ||
| 33 | mouse(kCGEventLeftMouseUp, at); | ||
| 34 | } else if (argc == 3 && !strcmp(argv[1], "type")) { | ||
| 35 | CFStringRef text = CFStringCreateWithCString(NULL, argv[2], kCFStringEncodingUTF8); | ||
| 36 | for (CFIndex i = 0; i < CFStringGetLength(text); i++) { | ||
| 37 | UniChar c = CFStringGetCharacterAtIndex(text, i); | ||
| 38 | key(0, 0, &c, 1); | ||
| 39 | } | ||
| 40 | CFRelease(text); | ||
| 41 | } else if ((argc == 3 || argc == 4) && !strcmp(argv[1], "key")) { | ||
| 42 | key(atoi(argv[2]), argc == 4 && !strcmp(argv[3], "command") ? kCGEventFlagMaskCommand : 0, NULL, 0); | ||
| 43 | } else { | ||
| 44 | fprintf(stderr, "usage: input click X Y | input type TEXT | input key KEYCODE [command]\n"); | ||
| 45 | return 2; | ||
| 46 | } | ||
| 47 | return 0; | ||
| 48 | } | ||
platform/snow-leopard/console/screen.c created+27| ... | @@ -0,0 +1,27 @@ | ||
| 1 | // Writes the main display to a PNG, for a session reached over ssh, where | ||
| 2 | // screencapture exits 0 without writing a file. Wakes a sleeping display first: asleep, | ||
| 3 | // it shows its last frame. | ||
| 4 | #include <ApplicationServices/ApplicationServices.h> | ||
| 5 | #include <CoreServices/CoreServices.h> | ||
| 6 | #include <stdio.h> | ||
| 7 | #include <string.h> | ||
| 8 | #include <unistd.h> | ||
| 9 | |||
| 10 | int main(int argc, char **argv) { | ||
| 11 | if (argc != 2) { | ||
| 12 | fprintf(stderr, "usage: screen OUT.png\n"); | ||
| 13 | return 2; | ||
| 14 | } | ||
| 15 | UpdateSystemActivity(UsrActivity); | ||
| 16 | sleep(2); | ||
| 17 | CGImageRef image = CGDisplayCreateImage(CGMainDisplayID()); | ||
| 18 | if (!image) { | ||
| 19 | fprintf(stderr, "screen: no image of the display\n"); | ||
| 20 | return 1; | ||
| 21 | } | ||
| 22 | CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *)argv[1], strlen(argv[1]), false); | ||
| 23 | CGImageDestinationRef file = CGImageDestinationCreateWithURL(url, CFSTR("public.png"), 1, NULL); | ||
| 24 | CGImageDestinationAddImage(file, image, NULL); | ||
| 25 | int ok = CGImageDestinationFinalize(file); | ||
| 26 | return ok ? 0 : 1; | ||
| 27 | } | ||
platform/snow-leopard/console/selectors.c created+45| ... | @@ -0,0 +1,45 @@ | ||
| 1 | // Reads selector names on stdin and prints those no class on this system implements, | ||
| 2 | // instance or class side: calls a 10.6 binary could make that would raise at run time. | ||
| 3 | // `strings BINARY` supplies candidates; names that were never selectors print too. | ||
| 4 | // Method lists are read directly: asking a class resolves methods, which some raise. | ||
| 5 | #include <dlfcn.h> | ||
| 6 | #include <objc/runtime.h> | ||
| 7 | #include <stdio.h> | ||
| 8 | #include <stdlib.h> | ||
| 9 | #include <string.h> | ||
| 10 | |||
| 11 | static int compare(const void *a, const void *b) { | ||
| 12 | return strcmp(*(const char *const *)a, *(const char *const *)b); | ||
| 13 | } | ||
| 14 | |||
| 15 | int main(void) { | ||
| 16 | const char *frameworks[] = {"AppKit", "Carbon", "QuartzCore", "Security", "OpenGL"}; | ||
| 17 | for (size_t i = 0; i < sizeof frameworks / sizeof *frameworks; i++) { | ||
| 18 | char path[256]; | ||
| 19 | snprintf(path, sizeof path, "/System/Library/Frameworks/%s.framework/%s", frameworks[i], frameworks[i]); | ||
| 20 | dlopen(path, RTLD_NOW); | ||
| 21 | } | ||
| 22 | int count = objc_getClassList(NULL, 0); | ||
| 23 | Class *classes = malloc(sizeof(Class) * count); | ||
| 24 | objc_getClassList(classes, count); | ||
| 25 | size_t names = 0, capacity = 1 << 16; | ||
| 26 | const char **known = malloc(sizeof(char *) * capacity); | ||
| 27 | for (int i = 0; i < count * 2; i++) { | ||
| 28 | Class cls = i < count ? classes[i] : object_getClass((id)classes[i - count]); | ||
| 29 | unsigned methods = 0; | ||
| 30 | Method *list = class_copyMethodList(cls, &methods); | ||
| 31 | for (unsigned m = 0; m < methods; m++) { | ||
| 32 | if (names == capacity) known = realloc(known, sizeof(char *) * (capacity *= 2)); | ||
| 33 | known[names++] = sel_getName(method_getName(list[m])); | ||
| 34 | } | ||
| 35 | free(list); | ||
| 36 | } | ||
| 37 | qsort(known, names, sizeof(char *), compare); | ||
| 38 | char line[1024]; | ||
| 39 | while (fgets(line, sizeof line, stdin)) { | ||
| 40 | line[strcspn(line, "\n")] = 0; | ||
| 41 | const char *name = line; | ||
| 42 | if (!bsearch(&name, known, names, sizeof(char *), compare)) puts(line); | ||
| 43 | } | ||
| 44 | return 0; | ||
| 45 | } | ||
platform/snow-leopard/gl21/Cargo.toml deleted-6| ... | @@ -1,6 +0,0 @@ | ||
| 1 | [package] | ||
| 2 | name = "gl21" | ||
| 3 | version = "0.0.0" | ||
| 4 | edition = "2024" | ||
| 5 | publish = false | ||
| 6 | description = "OpenGL 2.1 plus EXT_framebuffer_object, as Mac OS X 10.6 exports them." | ||
platform/snow-leopard/gl21/src/draw.glsl deleted-102| ... | @@ -1,102 +0,0 @@ | ||
| 1 | // GLSL 1.20 port of crates/draw/src/draw.wgsl. GLSL 1.20 has no flat interpolation, so | ||
| 2 | // the per-quad shape and stroke arrive interpolated between equal values: equality tests | ||
| 3 | // on them use a tolerance. | ||
| 4 | #ifdef VERTEX | ||
| 5 | attribute vec2 position; | ||
| 6 | attribute vec2 uv; | ||
| 7 | attribute vec4 color; | ||
| 8 | attribute vec2 local; | ||
| 9 | attribute vec4 shape; | ||
| 10 | attribute float stroke; | ||
| 11 | #endif | ||
| 12 | varying vec2 v_uv; | ||
| 13 | varying vec4 v_color; | ||
| 14 | varying vec2 v_local; | ||
| 15 | varying vec4 v_shape; | ||
| 16 | varying float v_stroke; | ||
| 17 | |||
| 18 | #ifdef VERTEX | ||
| 19 | void main() { | ||
| 20 | v_uv = uv; | ||
| 21 | v_color = color; | ||
| 22 | v_local = local; | ||
| 23 | v_shape = shape; | ||
| 24 | v_stroke = stroke; | ||
| 25 | gl_Position = vec4(position, 0.0, 1.0); | ||
| 26 | } | ||
| 27 | #else | ||
| 28 | uniform sampler2D atlas; | ||
| 29 | |||
| 30 | bool same(float a, float b) { | ||
| 31 | return abs(a - b) <= 1e-4 * max(1.0, max(abs(a), abs(b))); | ||
| 32 | } | ||
| 33 | |||
| 34 | float ellipse_arc(float angle, vec2 radius) { | ||
| 35 | if (same(radius.x, radius.y)) { | ||
| 36 | return angle * radius.x; | ||
| 37 | } | ||
| 38 | // Four-point Gauss-Legendre quadrature. | ||
| 39 | vec4 nodes = vec4(-0.86113631, -0.33998104, 0.33998104, 0.86113631); | ||
| 40 | vec4 weights = vec4(0.34785485, 0.65214515, 0.65214515, 0.34785485); | ||
| 41 | float half_angle = angle * 0.5; | ||
| 42 | float scale = max(radius.x, radius.y); | ||
| 43 | float sum = 0.0; | ||
| 44 | for (int i = 0; i < 4; i++) { | ||
| 45 | float t = half_angle * (nodes[i] + 1.0); | ||
| 46 | sum += weights[i] * length((radius / scale) * vec2(sin(t), cos(t))); | ||
| 47 | } | ||
| 48 | return half_angle * sum * scale; | ||
| 49 | } | ||
| 50 | |||
| 51 | void main() { | ||
| 52 | vec4 color = texture2D(atlas, v_uv) * v_color; | ||
| 53 | vec4 shape = v_shape; | ||
| 54 | if (shape.x > 0.0 && shape.y > 0.0) { | ||
| 55 | vec2 q = abs(v_local) - shape.xy + shape.zw; | ||
| 56 | float distance; | ||
| 57 | if (same(shape.z, shape.w)) { | ||
| 58 | distance = length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - shape.z; | ||
| 59 | } else if (all(greaterThan(q, vec2(0.0)))) { | ||
| 60 | vec2 radius = max(shape.zw, vec2(0.0001)); | ||
| 61 | vec2 normalized = q / radius; | ||
| 62 | float k0 = length(normalized); | ||
| 63 | distance = k0 * (k0 - 1.0) / max(length(normalized / radius), 1e-20); | ||
| 64 | } else { | ||
| 65 | vec2 edge = abs(v_local) - shape.xy; | ||
| 66 | distance = max(edge.x, edge.y); | ||
| 67 | } | ||
| 68 | float coverage = clamp(0.5 - distance, 0.0, 1.0); | ||
| 69 | float width = abs(v_stroke); | ||
| 70 | if (width > 1e-6) { | ||
| 71 | coverage *= clamp(distance + width + 0.5, 0.0, 1.0); | ||
| 72 | } | ||
| 73 | if (v_stroke < -1e-6) { | ||
| 74 | vec2 radius = max(shape.zw - width * 0.5, vec2(0.0)); | ||
| 75 | if (radius.x <= 1e-6 || radius.y <= 1e-6) { | ||
| 76 | radius = vec2(0.0); | ||
| 77 | } | ||
| 78 | vec2 straight = shape.xy - width * 0.5 - radius; | ||
| 79 | vec2 p = abs(v_local); | ||
| 80 | float quarter = straight.x + straight.y + ellipse_arc(1.57079632679, radius); | ||
| 81 | float along; | ||
| 82 | if (p.y <= straight.y) { | ||
| 83 | along = p.y; | ||
| 84 | } else if (p.x <= straight.x || radius.x == 0.0) { | ||
| 85 | along = quarter - p.x; | ||
| 86 | } else { | ||
| 87 | float angle = atan((p.y - straight.y) / radius.y, (p.x - straight.x) / radius.x); | ||
| 88 | along = straight.y + ellipse_arc(angle, radius); | ||
| 89 | } | ||
| 90 | if (v_local.x < 0.0) { | ||
| 91 | along = v_local.y < 0.0 ? 2.0 * quarter + along : 2.0 * quarter - along; | ||
| 92 | } else if (v_local.y < 0.0) { | ||
| 93 | along = 4.0 * quarter - along; | ||
| 94 | } | ||
| 95 | float phase = fract(along / (4.0 * width)) * (4.0 * width); | ||
| 96 | coverage *= clamp(width + 0.5 - abs(phase - 2.0 * width), 0.0, 1.0); | ||
| 97 | } | ||
| 98 | color *= coverage; | ||
| 99 | } | ||
| 100 | gl_FragColor = color; | ||
| 101 | } | ||
| 102 | #endif | ||
platform/snow-leopard/gl21/src/lib.rs deleted-118| ... | @@ -1,118 +0,0 @@ | ||
| 1 | //! OpenGL 2.1 and the extensions every Mac OS X 10.6 GPU driver exposes, linked straight | ||
| 2 | //! from OpenGL.framework, which exports them all; no loader is needed. | ||
| 3 | #![allow(non_snake_case, clippy::missing_safety_doc)] | ||
| 4 | |||
| 5 | pub mod painter; | ||
| 6 | |||
| 7 | use std::ffi::{c_char, c_void}; | ||
| 8 | |||
| 9 | pub type GLenum = u32; | ||
| 10 | pub type GLuint = u32; | ||
| 11 | pub type GLint = i32; | ||
| 12 | pub type GLsizei = i32; | ||
| 13 | pub type GLfloat = f32; | ||
| 14 | pub type GLboolean = u8; | ||
| 15 | pub type GLbitfield = u32; | ||
| 16 | pub type GLsizeiptr = isize; | ||
| 17 | |||
| 18 | pub const NO_ERROR: GLenum = 0; | ||
| 19 | pub const TRIANGLES: GLenum = 0x0004; | ||
| 20 | pub const COLOR_BUFFER_BIT: GLbitfield = 0x4000; | ||
| 21 | pub const BLEND: GLenum = 0x0BE2; | ||
| 22 | pub const SCISSOR_TEST: GLenum = 0x0C11; | ||
| 23 | pub const TEXTURE_2D: GLenum = 0x0DE1; | ||
| 24 | pub const UNSIGNED_BYTE: GLenum = 0x1401; | ||
| 25 | pub const FLOAT: GLenum = 0x1406; | ||
| 26 | pub const RGBA: GLenum = 0x1908; | ||
| 27 | pub const VENDOR: GLenum = 0x1F00; | ||
| 28 | pub const RENDERER: GLenum = 0x1F01; | ||
| 29 | pub const VERSION: GLenum = 0x1F02; | ||
| 30 | pub const EXTENSIONS: GLenum = 0x1F03; | ||
| 31 | pub const NEAREST: GLint = 0x2600; | ||
| 32 | pub const LINEAR: GLint = 0x2601; | ||
| 33 | pub const TEXTURE_MAG_FILTER: GLenum = 0x2800; | ||
| 34 | pub const TEXTURE_MIN_FILTER: GLenum = 0x2801; | ||
| 35 | pub const TEXTURE_WRAP_S: GLenum = 0x2802; | ||
| 36 | pub const TEXTURE_WRAP_T: GLenum = 0x2803; | ||
| 37 | pub const CLAMP_TO_EDGE: GLint = 0x812F; | ||
| 38 | pub const ONE: GLenum = 1; | ||
| 39 | pub const SRC_ALPHA: GLenum = 0x0302; | ||
| 40 | pub const ONE_MINUS_SRC_ALPHA: GLenum = 0x0303; | ||
| 41 | pub const TEXTURE0: GLenum = 0x84C0; | ||
| 42 | pub const ARRAY_BUFFER: GLenum = 0x8892; | ||
| 43 | pub const STREAM_DRAW: GLenum = 0x88E0; | ||
| 44 | pub const FRAGMENT_SHADER: GLenum = 0x8B30; | ||
| 45 | pub const VERTEX_SHADER: GLenum = 0x8B31; | ||
| 46 | pub const COMPILE_STATUS: GLenum = 0x8B81; | ||
| 47 | pub const LINK_STATUS: GLenum = 0x8B82; | ||
| 48 | pub const INFO_LOG_LENGTH: GLenum = 0x8B84; | ||
| 49 | pub const SRGB8_ALPHA8: GLint = 0x8C43; | ||
| 50 | pub const READ_FRAMEBUFFER: GLenum = 0x8CA8; | ||
| 51 | pub const DRAW_FRAMEBUFFER: GLenum = 0x8CA9; | ||
| 52 | pub const FRAMEBUFFER_COMPLETE: GLenum = 0x8CD5; | ||
| 53 | pub const COLOR_ATTACHMENT0: GLenum = 0x8CE0; | ||
| 54 | pub const FRAMEBUFFER: GLenum = 0x8D40; | ||
| 55 | pub const FRAMEBUFFER_SRGB: GLenum = 0x8DB9; | ||
| 56 | |||
| 57 | #[link(name = "OpenGL", kind = "framework")] | ||
| 58 | unsafe extern "C" { | ||
| 59 | pub fn glGetError() -> GLenum; | ||
| 60 | pub fn glGetString(name: GLenum) -> *const u8; | ||
| 61 | pub fn glEnable(cap: GLenum); | ||
| 62 | pub fn glDisable(cap: GLenum); | ||
| 63 | pub fn glViewport(x: GLint, y: GLint, width: GLsizei, height: GLsizei); | ||
| 64 | pub fn glScissor(x: GLint, y: GLint, width: GLsizei, height: GLsizei); | ||
| 65 | pub fn glClearColor(r: GLfloat, g: GLfloat, b: GLfloat, a: GLfloat); | ||
| 66 | pub fn glClear(mask: GLbitfield); | ||
| 67 | pub fn glFinish(); | ||
| 68 | pub fn glFlush(); | ||
| 69 | pub fn glBlendFuncSeparate(src_rgb: GLenum, dst_rgb: GLenum, src_a: GLenum, dst_a: GLenum); | ||
| 70 | pub fn glReadPixels(x: GLint, y: GLint, w: GLsizei, h: GLsizei, format: GLenum, ty: GLenum, data: *mut c_void); | ||
| 71 | pub fn glPixelStorei(name: GLenum, value: GLint); | ||
| 72 | |||
| 73 | pub fn glGenTextures(n: GLsizei, textures: *mut GLuint); | ||
| 74 | pub fn glDeleteTextures(n: GLsizei, textures: *const GLuint); | ||
| 75 | pub fn glBindTexture(target: GLenum, texture: GLuint); | ||
| 76 | pub fn glActiveTexture(unit: GLenum); | ||
| 77 | pub fn glTexParameteri(target: GLenum, name: GLenum, value: GLint); | ||
| 78 | pub fn glTexImage2D(target: GLenum, level: GLint, internal: GLint, w: GLsizei, h: GLsizei, border: GLint, format: GLenum, ty: GLenum, data: *const c_void); | ||
| 79 | pub fn glTexSubImage2D(target: GLenum, level: GLint, x: GLint, y: GLint, w: GLsizei, h: GLsizei, format: GLenum, ty: GLenum, data: *const c_void); | ||
| 80 | |||
| 81 | pub fn glGenBuffers(n: GLsizei, buffers: *mut GLuint); | ||
| 82 | pub fn glBindBuffer(target: GLenum, buffer: GLuint); | ||
| 83 | pub fn glBufferData(target: GLenum, size: GLsizeiptr, data: *const c_void, usage: GLenum); | ||
| 84 | pub fn glEnableVertexAttribArray(index: GLuint); | ||
| 85 | pub fn glVertexAttribPointer(index: GLuint, size: GLint, ty: GLenum, normalized: GLboolean, stride: GLsizei, offset: *const c_void); | ||
| 86 | pub fn glDrawArrays(mode: GLenum, first: GLint, count: GLsizei); | ||
| 87 | |||
| 88 | pub fn glCreateShader(kind: GLenum) -> GLuint; | ||
| 89 | pub fn glShaderSource(shader: GLuint, count: GLsizei, sources: *const *const c_char, lengths: *const GLint); | ||
| 90 | pub fn glCompileShader(shader: GLuint); | ||
| 91 | pub fn glGetShaderiv(shader: GLuint, name: GLenum, value: *mut GLint); | ||
| 92 | pub fn glGetShaderInfoLog(shader: GLuint, size: GLsizei, length: *mut GLsizei, log: *mut c_char); | ||
| 93 | pub fn glCreateProgram() -> GLuint; | ||
| 94 | pub fn glAttachShader(program: GLuint, shader: GLuint); | ||
| 95 | pub fn glBindAttribLocation(program: GLuint, index: GLuint, name: *const c_char); | ||
| 96 | pub fn glLinkProgram(program: GLuint); | ||
| 97 | pub fn glGetProgramiv(program: GLuint, name: GLenum, value: *mut GLint); | ||
| 98 | pub fn glGetProgramInfoLog(program: GLuint, size: GLsizei, length: *mut GLsizei, log: *mut c_char); | ||
| 99 | pub fn glUseProgram(program: GLuint); | ||
| 100 | pub fn glGetUniformLocation(program: GLuint, name: *const c_char) -> GLint; | ||
| 101 | pub fn glUniform1i(location: GLint, value: GLint); | ||
| 102 | |||
| 103 | pub fn glGenFramebuffersEXT(n: GLsizei, framebuffers: *mut GLuint); | ||
| 104 | pub fn glDeleteFramebuffersEXT(n: GLsizei, framebuffers: *const GLuint); | ||
| 105 | pub fn glBindFramebufferEXT(target: GLenum, framebuffer: GLuint); | ||
| 106 | pub fn glFramebufferTexture2DEXT(target: GLenum, attachment: GLenum, textarget: GLenum, texture: GLuint, level: GLint); | ||
| 107 | pub fn glCheckFramebufferStatusEXT(target: GLenum) -> GLenum; | ||
| 108 | pub fn glBlitFramebufferEXT(sx0: GLint, sy0: GLint, sx1: GLint, sy1: GLint, dx0: GLint, dy0: GLint, dx1: GLint, dy1: GLint, mask: GLbitfield, filter: GLenum); | ||
| 109 | } | ||
| 110 | |||
| 111 | /// A GL string such as `VERSION` or `EXTENSIONS`, empty without a current context. | ||
| 112 | pub fn string(name: GLenum) -> String { | ||
| 113 | let text = unsafe { glGetString(name) }; | ||
| 114 | if text.is_null() { | ||
| 115 | return String::new(); | ||
| 116 | } | ||
| 117 | unsafe { std::ffi::CStr::from_ptr(text.cast()) }.to_string_lossy().into_owned() | ||
| 118 | } | ||
platform/snow-leopard/gl21/src/painter.rs deleted-272| ... | @@ -1,272 +0,0 @@ | ||
| 1 | //! The submission half of `draw::Renderer` on OpenGL 2.1: the atlas, image textures, one | ||
| 2 | //! vertex buffer and the ported shader. `draw` keeps building vertices and batches; this | ||
| 3 | //! draws them into an sRGB framebuffer object, as wgpu draws them into its target. | ||
| 4 | use crate::*; | ||
| 5 | use std::{ffi::CString, ops::Range, ptr}; | ||
| 6 | |||
| 7 | /// Laid out as `draw`'s vertex. | ||
| 8 | #[repr(C)] | ||
| 9 | #[derive(Clone, Copy, Debug, Default)] | ||
| 10 | pub struct Vertex { | ||
| 11 | pub position: [f32; 2], | ||
| 12 | pub uv: [f32; 2], | ||
| 13 | pub color: [f32; 4], | ||
| 14 | pub local: [f32; 2], | ||
| 15 | pub shape: [f32; 4], | ||
| 16 | pub stroke: f32, | ||
| 17 | } | ||
| 18 | |||
| 19 | pub struct Batch<'a> { | ||
| 20 | pub vertices: Range<u32>, | ||
| 21 | /// None samples the atlas with straight alpha; an image is premultiplied. | ||
| 22 | pub image: Option<&'a Texture>, | ||
| 23 | /// Device pixels from the top left: `[x, y, width, height]`. | ||
| 24 | pub scissor: [u32; 4], | ||
| 25 | } | ||
| 26 | |||
| 27 | /// An sRGB RGBA texture. | ||
| 28 | pub struct Texture { | ||
| 29 | name: GLuint, | ||
| 30 | size: [u32; 2], | ||
| 31 | } | ||
| 32 | |||
| 33 | impl Texture { | ||
| 34 | fn new(size: [u32; 2], filter: GLint, pixels: Option<&[u8]>) -> Self { | ||
| 35 | let mut name = 0; | ||
| 36 | unsafe { | ||
| 37 | glGenTextures(1, &mut name); | ||
| 38 | glBindTexture(TEXTURE_2D, name); | ||
| 39 | glTexParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, filter); | ||
| 40 | glTexParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, filter); | ||
| 41 | glTexParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE); | ||
| 42 | glTexParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE); | ||
| 43 | glTexImage2D( | ||
| 44 | TEXTURE_2D, | ||
| 45 | 0, | ||
| 46 | SRGB8_ALPHA8, | ||
| 47 | size[0] as GLsizei, | ||
| 48 | size[1] as GLsizei, | ||
| 49 | 0, | ||
| 50 | RGBA, | ||
| 51 | UNSIGNED_BYTE, | ||
| 52 | pixels.map_or(ptr::null(), |p| p.as_ptr().cast()), | ||
| 53 | ); | ||
| 54 | } | ||
| 55 | Self { name, size } | ||
| 56 | } | ||
| 57 | |||
| 58 | /// Writes `rgba` rows, top first, at `origin`. | ||
| 59 | pub fn write(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { | ||
| 60 | assert_eq!(rgba.len(), (size[0] * size[1] * 4) as usize); | ||
| 61 | unsafe { | ||
| 62 | glBindTexture(TEXTURE_2D, self.name); | ||
| 63 | glTexSubImage2D( | ||
| 64 | TEXTURE_2D, | ||
| 65 | 0, | ||
| 66 | origin[0] as GLint, | ||
| 67 | origin[1] as GLint, | ||
| 68 | size[0] as GLsizei, | ||
| 69 | size[1] as GLsizei, | ||
| 70 | RGBA, | ||
| 71 | UNSIGNED_BYTE, | ||
| 72 | rgba.as_ptr().cast(), | ||
| 73 | ); | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | pub fn size(&self) -> [u32; 2] { | ||
| 78 | self.size | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | impl Drop for Texture { | ||
| 83 | fn drop(&mut self) { | ||
| 84 | unsafe { glDeleteTextures(1, &self.name) }; | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | /// An offscreen sRGB colour target; blending happens in linear light. | ||
| 89 | pub struct Target { | ||
| 90 | framebuffer: GLuint, | ||
| 91 | texture: Texture, | ||
| 92 | } | ||
| 93 | |||
| 94 | impl Target { | ||
| 95 | pub fn new(size: [u32; 2]) -> Result<Self, String> { | ||
| 96 | let texture = Texture::new(size, NEAREST, None); | ||
| 97 | let mut framebuffer = 0; | ||
| 98 | unsafe { | ||
| 99 | glGenFramebuffersEXT(1, &mut framebuffer); | ||
| 100 | glBindFramebufferEXT(FRAMEBUFFER, framebuffer); | ||
| 101 | glFramebufferTexture2DEXT(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, texture.name, 0); | ||
| 102 | let status = glCheckFramebufferStatusEXT(FRAMEBUFFER); | ||
| 103 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 104 | if status != FRAMEBUFFER_COMPLETE { | ||
| 105 | return Err(format!("framebuffer incomplete: {status:#x}")); | ||
| 106 | } | ||
| 107 | } | ||
| 108 | Ok(Self { framebuffer, texture }) | ||
| 109 | } | ||
| 110 | |||
| 111 | pub fn size(&self) -> [u32; 2] { | ||
| 112 | self.texture.size | ||
| 113 | } | ||
| 114 | |||
| 115 | /// Copies the target to the current context's window framebuffer, same size. | ||
| 116 | pub fn present(&self) { | ||
| 117 | let [w, h] = self.texture.size.map(|v| v as GLint); | ||
| 118 | unsafe { | ||
| 119 | glBindFramebufferEXT(READ_FRAMEBUFFER, self.framebuffer); | ||
| 120 | glBindFramebufferEXT(DRAW_FRAMEBUFFER, 0); | ||
| 121 | glBlitFramebufferEXT(0, 0, w, h, 0, 0, w, h, COLOR_BUFFER_BIT, NEAREST as GLenum); | ||
| 122 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | /// sRGB-encoded RGBA rows, top first. | ||
| 127 | pub fn read_pixels(&self) -> Vec<u8> { | ||
| 128 | let [w, h] = self.texture.size; | ||
| 129 | let mut pixels = vec![0u8; (w * h * 4) as usize]; | ||
| 130 | unsafe { | ||
| 131 | glBindFramebufferEXT(FRAMEBUFFER, self.framebuffer); | ||
| 132 | glReadPixels(0, 0, w as GLsizei, h as GLsizei, RGBA, UNSIGNED_BYTE, pixels.as_mut_ptr().cast()); | ||
| 133 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 134 | } | ||
| 135 | pixels.chunks(w as usize * 4).rev().flatten().copied().collect() | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | impl Drop for Target { | ||
| 140 | fn drop(&mut self) { | ||
| 141 | unsafe { glDeleteFramebuffersEXT(1, &self.framebuffer) }; | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | pub struct Painter { | ||
| 146 | program: GLuint, | ||
| 147 | buffer: GLuint, | ||
| 148 | pub atlas: Texture, | ||
| 149 | } | ||
| 150 | |||
| 151 | const ATTRIBUTES: [(&str, GLint, usize); 6] = [ | ||
| 152 | ("position", 2, 0), | ||
| 153 | ("uv", 2, 8), | ||
| 154 | ("color", 4, 16), | ||
| 155 | ("local", 2, 32), | ||
| 156 | ("shape", 4, 40), | ||
| 157 | ("stroke", 1, 56), | ||
| 158 | ]; | ||
| 159 | |||
| 160 | impl Painter { | ||
| 161 | /// Needs a current context. | ||
| 162 | pub fn new(atlas_size: u32) -> Result<Self, String> { | ||
| 163 | let source = include_str!("draw.glsl"); | ||
| 164 | let program = unsafe { | ||
| 165 | let vertex = compile(VERTEX_SHADER, &format!("#version 120\n#define VERTEX\n{source}"))?; | ||
| 166 | let fragment = compile(FRAGMENT_SHADER, &format!("#version 120\n{source}"))?; | ||
| 167 | let program = glCreateProgram(); | ||
| 168 | glAttachShader(program, vertex); | ||
| 169 | glAttachShader(program, fragment); | ||
| 170 | for (index, (name, _, _)) in ATTRIBUTES.iter().enumerate() { | ||
| 171 | let name = CString::new(*name).unwrap(); | ||
| 172 | glBindAttribLocation(program, index as GLuint, name.as_ptr()); | ||
| 173 | } | ||
| 174 | glLinkProgram(program); | ||
| 175 | let mut linked = 0; | ||
| 176 | glGetProgramiv(program, LINK_STATUS, &mut linked); | ||
| 177 | if linked == 0 { | ||
| 178 | return Err(format!("link: {}", log(program, glGetProgramiv, glGetProgramInfoLog))); | ||
| 179 | } | ||
| 180 | program | ||
| 181 | }; | ||
| 182 | let atlas = Texture::new([atlas_size; 2], NEAREST, None); | ||
| 183 | // The atlas's first texel is the white that solid fills sample. | ||
| 184 | atlas.write([0, 0], [1, 1], &[255; 4]); | ||
| 185 | let mut buffer = 0; | ||
| 186 | unsafe { glGenBuffers(1, &mut buffer) }; | ||
| 187 | Ok(Self { program, buffer, atlas }) | ||
| 188 | } | ||
| 189 | |||
| 190 | /// An image texture, filtered linearly; pixels are premultiplied rows, top first. | ||
| 191 | pub fn image(&self, size: [u32; 2], rgba: &[u8]) -> Texture { | ||
| 192 | Texture::new(size, LINEAR, Some(rgba)) | ||
| 193 | } | ||
| 194 | |||
| 195 | /// Clears `target` to linear `clear` and draws the batches in order. | ||
| 196 | pub fn draw(&self, target: &Target, clear: [f32; 4], vertices: &[Vertex], batches: &[Batch<'_>]) { | ||
| 197 | let [width, height] = target.size().map(|v| v as GLsizei); | ||
| 198 | unsafe { | ||
| 199 | glBindFramebufferEXT(FRAMEBUFFER, target.framebuffer); | ||
| 200 | glEnable(FRAMEBUFFER_SRGB); | ||
| 201 | glViewport(0, 0, width, height); | ||
| 202 | glDisable(SCISSOR_TEST); | ||
| 203 | glClearColor(clear[0], clear[1], clear[2], clear[3]); | ||
| 204 | glClear(COLOR_BUFFER_BIT); | ||
| 205 | glUseProgram(self.program); | ||
| 206 | glUniform1i(glGetUniformLocation(self.program, c"atlas".as_ptr()), 0); | ||
| 207 | glActiveTexture(TEXTURE0); | ||
| 208 | glBindBuffer(ARRAY_BUFFER, self.buffer); | ||
| 209 | glBufferData(ARRAY_BUFFER, size_of_val(vertices) as GLsizeiptr, vertices.as_ptr().cast(), STREAM_DRAW); | ||
| 210 | for (index, (_, size, offset)) in ATTRIBUTES.iter().enumerate() { | ||
| 211 | glEnableVertexAttribArray(index as GLuint); | ||
| 212 | glVertexAttribPointer( | ||
| 213 | index as GLuint, | ||
| 214 | *size, | ||
| 215 | FLOAT, | ||
| 216 | 0, | ||
| 217 | size_of::<Vertex>() as GLsizei, | ||
| 218 | ptr::without_provenance(*offset), | ||
| 219 | ); | ||
| 220 | } | ||
| 221 | glEnable(BLEND); | ||
| 222 | glEnable(SCISSOR_TEST); | ||
| 223 | for batch in batches { | ||
| 224 | let [x, y, w, h] = batch.scissor.map(|v| v as GLint); | ||
| 225 | glScissor(x, height - y - h, w, h); | ||
| 226 | let (texture, source) = match batch.image { | ||
| 227 | Some(image) => (image.name, ONE), | ||
| 228 | None => (self.atlas.name, SRC_ALPHA), | ||
| 229 | }; | ||
| 230 | glBlendFuncSeparate(source, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA); | ||
| 231 | glBindTexture(TEXTURE_2D, texture); | ||
| 232 | glDrawArrays( | ||
| 233 | TRIANGLES, | ||
| 234 | batch.vertices.start as GLint, | ||
| 235 | batch.vertices.len() as GLsizei, | ||
| 236 | ); | ||
| 237 | } | ||
| 238 | glDisable(SCISSOR_TEST); | ||
| 239 | glDisable(BLEND); | ||
| 240 | glBindFramebufferEXT(FRAMEBUFFER, 0); | ||
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | unsafe fn compile(kind: GLenum, source: &str) -> Result<GLuint, String> { | ||
| 246 | unsafe { | ||
| 247 | let shader = glCreateShader(kind); | ||
| 248 | let text = CString::new(source).unwrap(); | ||
| 249 | glShaderSource(shader, 1, &text.as_ptr(), ptr::null()); | ||
| 250 | glCompileShader(shader); | ||
| 251 | let mut compiled = 0; | ||
| 252 | glGetShaderiv(shader, COMPILE_STATUS, &mut compiled); | ||
| 253 | if compiled == 0 { | ||
| 254 | return Err(format!("compile: {}", log(shader, glGetShaderiv, glGetShaderInfoLog))); | ||
| 255 | } | ||
| 256 | Ok(shader) | ||
| 257 | } | ||
| 258 | } | ||
| 259 | |||
| 260 | unsafe fn log( | ||
| 261 | object: GLuint, | ||
| 262 | get: unsafe extern "C" fn(GLuint, GLenum, *mut GLint), | ||
| 263 | read: unsafe extern "C" fn(GLuint, GLsizei, *mut GLsizei, *mut std::ffi::c_char), | ||
| 264 | ) -> String { | ||
| 265 | unsafe { | ||
| 266 | let mut length = 0; | ||
| 267 | get(object, INFO_LOG_LENGTH, &mut length); | ||
| 268 | let mut text = vec![0u8; length.max(1) as usize]; | ||
| 269 | read(object, length, ptr::null_mut(), text.as_mut_ptr().cast()); | ||
| 270 | String::from_utf8_lossy(&text).trim_end_matches('\0').to_owned() | ||
| 271 | } | ||
| 272 | } | ||
platform/snow-leopard/link.sh+2-2| ... | @@ -17,8 +17,8 @@ for arg; do | ... | @@ -17,8 +17,8 @@ for arg; do |
| 17 | if [ "$prev" = -framework ]; then | 17 | if [ "$prev" = -framework ]; then |
| 18 | case "$arg" in | 18 | case "$arg" in |
| 19 | CoreGraphics | CoreText | ImageIO | ColorSync) arg=ApplicationServices ;; | 19 | CoreGraphics | CoreText | ImageIO | ColorSync) arg=ApplicationServices ;; |
| 20 | # Until draw's wgpu backend is optional: wgpu names Metal, which 10.6 lacks, | 20 | # Test binaries whose dev-dependencies take draw's wgpu backend (ui, canvas) |
| 21 | # and nothing a 10.6 binary reaches calls into it. | 21 | # name Metal, which 10.6 lacks; nothing they run on 10.6 calls into it. |
| 22 | Metal) arg=Foundation ;; | 22 | Metal) arg=Foundation ;; |
| 23 | esac | 23 | esac |
| 24 | fi | 24 | fi |
platform/snow-leopard/probe/Cargo.toml+8-7| ... | @@ -5,20 +5,21 @@ edition = "2024" | ... | @@ -5,20 +5,21 @@ edition = "2024" |
| 5 | publish = false | 5 | publish = false |
| 6 | 6 | ||
| 7 | [dependencies] | 7 | [dependencies] |
| 8 | gl21 = { path = "../gl21" } | 8 | draw = { path = "../../../crates/draw", default-features = false, features = ["render"] } |
| 9 | objc2.workspace = true | 9 | objc2.workspace = true |
| 10 | objc2-foundation = { workspace = true, features = ["std", "NSString", "NSAutoreleasePool", "NSObject", "NSZone", "NSGeometry", "NSDate", "NSRunLoop"] } | 10 | objc2-foundation = { workspace = true, features = ["std", "NSString", "NSObject", "NSZone"] } |
| 11 | objc2-app-kit = { workspace = true, features = ["std", "NSApplication", "NSResponder", "NSWindow", "NSView", "NSOpenGL", "NSOpenGLView", "NSEvent", "NSRunningApplication", "NSGraphics"] } | 11 | objc2-app-kit = { workspace = true, features = ["std", "NSOpenGL"] } |
| 12 | png = "0.18" | 12 | png = "0.18" |
| 13 | ui = { path = "../../../crates/ui" } | ||
| 13 | 14 | ||
| 14 | [[bin]] | 15 | [[bin]] |
| 15 | name = "probe" | 16 | name = "probe" |
| 16 | path = "src/main.rs" | 17 | path = "src/main.rs" |
| 17 | 18 | ||
| 18 | [[bin]] | ||
| 19 | name = "window-probe" | ||
| 20 | path = "src/window.rs" | ||
| 21 | |||
| 22 | [[bin]] | 19 | [[bin]] |
| 23 | name = "render-probe" | 20 | name = "render-probe" |
| 24 | path = "src/render.rs" | 21 | path = "src/render.rs" |
| 22 | |||
| 23 | [[bin]] | ||
| 24 | name = "text-probe" | ||
| 25 | path = "src/text.rs" |
platform/snow-leopard/probe/src/context.rs created+15| ... | @@ -0,0 +1,15 @@ | ||
| 1 | //! An offscreen OpenGL context, current on this thread. | ||
| 2 | use objc2::{MainThreadMarker, rc::Retained}; | ||
| 3 | use objc2_app_kit::{NSOpenGLContext, NSOpenGLPFAAccelerated, NSOpenGLPFAColorSize, NSOpenGLPixelFormat}; | ||
| 4 | |||
| 5 | pub fn current() -> Retained<NSOpenGLContext> { | ||
| 6 | let mtm = MainThreadMarker::new().unwrap(); | ||
| 7 | let attributes = [NSOpenGLPFAAccelerated, NSOpenGLPFAColorSize, 24, 0]; | ||
| 8 | let format = unsafe { | ||
| 9 | NSOpenGLPixelFormat::initWithAttributes(mtm.alloc(), std::ptr::NonNull::new(attributes.as_ptr().cast_mut()).unwrap()) | ||
| 10 | } | ||
| 11 | .expect("pixel format"); | ||
| 12 | let context = NSOpenGLContext::initWithFormat_shareContext(mtm.alloc(), &format, None).expect("context"); | ||
| 13 | context.makeCurrentContext(); | ||
| 14 | context | ||
| 15 | } | ||
platform/snow-leopard/probe/src/main.rs+1-1| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | use objc2::{rc::{Retained, autoreleasepool}, runtime::NSObjectProtocol}; | 1 | use objc2::rc::{Retained, autoreleasepool}; |
| 2 | use objc2_foundation::NSCopying; | 2 | use objc2_foundation::NSCopying; |
| 3 | use objc2_foundation::{NSObject, NSString}; | 3 | use objc2_foundation::{NSObject, NSString}; |
| 4 | 4 |
platform/snow-leopard/probe/src/render.rs+10-89| ... | @@ -1,102 +1,23 @@ | ... | @@ -1,102 +1,23 @@ |
| 1 | //! Paints `scene` through the GL 2.1 painter into an offscreen target and writes | 1 | //! Paints `scene` through `draw` over OpenGL into an offscreen target and writes |
| 2 | //! `render-probe.png`. The vertices follow `draw::Renderer`'s quad and rounded-rectangle | 2 | //! `render-probe.png`. |
| 3 | //! encoding at scale 1. | ||
| 4 | #![allow(deprecated)] // OpenGL is the only GPU API 10.6 has. | 3 | #![allow(deprecated)] // OpenGL is the only GPU API 10.6 has. |
| 4 | mod context; | ||
| 5 | mod scene; | 5 | mod scene; |
| 6 | 6 | ||
| 7 | use gl21::painter::{Batch, Painter, Target, Vertex}; | ||
| 8 | use objc2::MainThreadMarker; | ||
| 9 | use objc2_app_kit::{NSOpenGLContext, NSOpenGLPFAAccelerated, NSOpenGLPFAColorSize, NSOpenGLPixelFormat}; | ||
| 10 | use scene::Shape; | ||
| 11 | |||
| 12 | const WHITE_UV: [f32; 4] = [0.5 / ATLAS as f32; 4]; | ||
| 13 | const ATLAS: u32 = 2048; | ||
| 14 | |||
| 15 | fn quad(vertices: &mut Vec<Vertex>, rect: [f32; 4], uv: [f32; 4], color: [f32; 4]) { | ||
| 16 | let [w, h] = scene::SIZE.map(|v| v as f32); | ||
| 17 | let (x0, x1) = (rect[0] * 2.0 / w - 1.0, rect[2] * 2.0 / w - 1.0); | ||
| 18 | let (y0, y1) = (1.0 - rect[1] * 2.0 / h, 1.0 - rect[3] * 2.0 / h); | ||
| 19 | let [hx, hy] = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | ||
| 20 | for (position, uv, local) in [ | ||
| 21 | ([x0, y0], [uv[0], uv[1]], [-hx, -hy]), | ||
| 22 | ([x0, y1], [uv[0], uv[3]], [-hx, hy]), | ||
| 23 | ([x1, y1], [uv[2], uv[3]], [hx, hy]), | ||
| 24 | ([x0, y0], [uv[0], uv[1]], [-hx, -hy]), | ||
| 25 | ([x1, y1], [uv[2], uv[3]], [hx, hy]), | ||
| 26 | ([x1, y0], [uv[2], uv[1]], [hx, -hy]), | ||
| 27 | ] { | ||
| 28 | vertices.push(Vertex { position, uv, color, local, ..Default::default() }); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | fn rounded(vertices: &mut Vec<Vertex>, rect: [f32; 4], radius: [f32; 2], stroke: Option<(f32, bool)>, colors: [[f32; 4]; 2]) { | ||
| 33 | let start = vertices.len(); | ||
| 34 | quad(vertices, [rect[0] - 1.0, rect[1] - 1.0, rect[2] + 1.0, rect[3] + 1.0], WHITE_UV, colors[0]); | ||
| 35 | let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | ||
| 36 | let radius = if radius.contains(&0.0) { [0.0; 2] } else { [radius[0].min(half[0]), radius[1].min(half[1])] }; | ||
| 37 | let width = stroke.map_or(0.0, |(width, dashed)| { | ||
| 38 | let width = width.min(half[0].min(half[1])); | ||
| 39 | if dashed { -width } else { width } | ||
| 40 | }); | ||
| 41 | for vertex in &mut vertices[start..] { | ||
| 42 | vertex.shape = [half[0], half[1], radius[0], radius[1]]; | ||
| 43 | vertex.stroke = width; | ||
| 44 | if vertex.local[1] > 0.0 { | ||
| 45 | vertex.color = colors[1]; | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | fn main() { | 7 | fn main() { |
| 51 | objc2::rc::autoreleasepool(|_| run()); | 8 | objc2::rc::autoreleasepool(|_| run()); |
| 52 | } | 9 | } |
| 53 | 10 | ||
| 54 | fn run() { | 11 | fn run() { |
| 55 | let mtm = MainThreadMarker::new().unwrap(); | 12 | let _context = context::current(); |
| 56 | let attributes = [NSOpenGLPFAAccelerated, NSOpenGLPFAColorSize, 24, 0]; | 13 | let mut renderer = draw::Renderer::new().unwrap_or_else(|error| panic!("{error}")); |
| 57 | let format = unsafe { | 14 | let target = draw::Target::new(scene::SIZE).unwrap(); |
| 58 | NSOpenGLPixelFormat::initWithAttributes(mtm.alloc(), std::ptr::NonNull::new(attributes.as_ptr().cast_mut()).unwrap()) | 15 | let checker = scene::checker(); |
| 59 | } | 16 | let primitives = scene::primitives(&checker); |
| 60 | .expect("pixel format"); | ||
| 61 | let context = NSOpenGLContext::initWithFormat_shareContext(mtm.alloc(), &format, None).expect("context"); | ||
| 62 | context.makeCurrentContext(); | ||
| 63 | |||
| 64 | let painter = Painter::new(ATLAS).unwrap_or_else(|error| panic!("{error}")); | ||
| 65 | let target = Target::new(scene::SIZE).unwrap(); | ||
| 66 | let (checker_size, checker) = scene::checker(); | ||
| 67 | let image = painter.image(checker_size, &checker); | ||
| 68 | let mut vertices = Vec::new(); | ||
| 69 | let mut spans = Vec::new(); | ||
| 70 | for shape in scene::shapes() { | ||
| 71 | let start = vertices.len() as u32; | ||
| 72 | let is_image = match shape { | ||
| 73 | Shape::Rect { rect, color } => { | ||
| 74 | quad(&mut vertices, rect, WHITE_UV, color); | ||
| 75 | false | ||
| 76 | } | ||
| 77 | Shape::Rounded { rect, radius, stroke, colors } => { | ||
| 78 | rounded(&mut vertices, rect, radius, stroke, colors); | ||
| 79 | false | ||
| 80 | } | ||
| 81 | Shape::Image { rect } => { | ||
| 82 | quad(&mut vertices, rect, [0.0, 0.0, 1.0, 1.0], [1.0; 4]); | ||
| 83 | true | ||
| 84 | } | ||
| 85 | }; | ||
| 86 | spans.push((start..vertices.len() as u32, is_image)); | ||
| 87 | } | ||
| 88 | let batches: Vec<Batch> = spans | ||
| 89 | .into_iter() | ||
| 90 | .map(|(vertices, is_image)| Batch { | ||
| 91 | vertices, | ||
| 92 | image: is_image.then_some(&image), | ||
| 93 | scissor: [0, 0, scene::SIZE[0], scene::SIZE[1]], | ||
| 94 | }) | ||
| 95 | .collect(); | ||
| 96 | let started = std::time::Instant::now(); | 17 | let started = std::time::Instant::now(); |
| 97 | painter.draw(&target, scene::CLEAR, &vertices, &batches); | 18 | renderer.draw(&target, scene::SIZE, scene::CLEAR, &[scene::layer(&primitives)]).unwrap(); |
| 98 | let pixels = target.read_pixels(); | 19 | let pixels = target.read_pixels(); |
| 99 | println!("drew {} vertices in {:?}; GL error {:#x}", vertices.len(), started.elapsed(), unsafe { gl21::glGetError() }); | 20 | println!("drew in {:?}", started.elapsed()); |
| 100 | let file = std::fs::File::create("render-probe.png").unwrap(); | 21 | let file = std::fs::File::create("render-probe.png").unwrap(); |
| 101 | let mut encoder = png::Encoder::new(file, scene::SIZE[0], scene::SIZE[1]); | 22 | let mut encoder = png::Encoder::new(file, scene::SIZE[0], scene::SIZE[1]); |
| 102 | encoder.set_color(png::ColorType::Rgba); | 23 | encoder.set_color(png::ColorType::Rgba); |
platform/snow-leopard/probe/src/scene.rs+57-23| ... | @@ -1,40 +1,74 @@ | ... | @@ -1,40 +1,74 @@ |
| 1 | //! One test scene, painted by `draw` over wgpu (the `reference` crate) and by the GL 2.1 | 1 | //! One scene painted by `draw` over wgpu on the dev Mac (`reference`) and over OpenGL on |
| 2 | //! painter on 10.6 (`render-probe`), whose PNGs `compare.py` diffs. Device pixels, | 2 | //! 10.6 (`render-probe`), whose PNGs `reference --compare` diffs. Device pixels, linear |
| 3 | //! linear colours. | 3 | //! colours. |
| 4 | #![allow(dead_code)] // Each binary paints part of it. | ||
| 5 | use draw::{Layer, Primitive, RasterImage, Stroke}; | ||
| 4 | 6 | ||
| 5 | pub const SIZE: [u32; 2] = [400, 300]; | 7 | pub const SIZE: [u32; 2] = [400, 300]; |
| 6 | pub const CLEAR: [f32; 4] = [0.85, 0.85, 0.85, 1.0]; | 8 | pub const CLEAR: [f32; 4] = [0.85, 0.85, 0.85, 1.0]; |
| 7 | 9 | ||
| 8 | pub enum Shape { | 10 | /// A 4×4 opaque sRGB checker, stretched and filtered by the image path. |
| 9 | Rect { rect: [f32; 4], color: [f32; 4] }, | 11 | pub fn checker() -> RasterImage { |
| 10 | /// `stroke` is a width and whether it is dashed. | 12 | let pixels = (0..16) |
| 11 | Rounded { rect: [f32; 4], radius: [f32; 2], stroke: Option<(f32, bool)>, colors: [[f32; 4]; 2] }, | 13 | .flat_map(|i| if (i % 4 + i / 4) % 2 == 0 { [240, 200, 40, 255] } else { [30, 120, 90, 255] }) |
| 12 | Image { rect: [f32; 4] }, | 14 | .collect(); |
| 15 | RasterImage::new([4, 4], pixels).unwrap() | ||
| 13 | } | 16 | } |
| 14 | 17 | ||
| 15 | pub fn shapes() -> Vec<Shape> { | 18 | pub fn primitives(checker: &RasterImage) -> Vec<Primitive<'_>> { |
| 16 | let red = [0.6, 0.05, 0.05, 1.0]; | 19 | let red = [0.6, 0.05, 0.05, 1.0]; |
| 17 | let blue = [0.05, 0.1, 0.6, 1.0]; | 20 | let blue = [0.05, 0.1, 0.6, 1.0]; |
| 18 | let green = [0.05, 0.4, 0.1, 1.0]; | 21 | let green = [0.05, 0.4, 0.1, 1.0]; |
| 19 | let ink = [0.02, 0.02, 0.02, 1.0]; | 22 | let ink = [0.02, 0.02, 0.02, 1.0]; |
| 20 | use Shape::*; | 23 | use Primitive::*; |
| 21 | vec![ | 24 | vec![ |
| 22 | Rect { rect: [20.0, 20.0, 120.0, 80.0], color: red }, | 25 | Rect { rect: [20.0, 20.0, 120.0, 80.0], color: red }, |
| 23 | Rounded { rect: [140.0, 20.0, 260.0, 80.0], radius: [12.0, 12.0], stroke: None, colors: [blue; 2] }, | 26 | Gradient { rect: [140.0, 20.0, 260.0, 80.0], radius: [12.0, 12.0], colors: [blue; 2] }, |
| 24 | Rounded { rect: [280.0, 20.5, 380.0, 80.25], radius: [30.0, 12.0], stroke: None, colors: [green; 2] }, | 27 | Gradient { rect: [280.0, 20.5, 380.0, 80.25], radius: [30.0, 12.0], colors: [green; 2] }, |
| 25 | Rounded { rect: [20.0, 100.0, 120.0, 160.0], radius: [10.0, 10.0], stroke: Some((3.0, false)), colors: [ink; 2] }, | 28 | RoundedRect { rect: [20.0, 100.0, 120.0, 160.0], radius: [10.0, 10.0], stroke: Some(Stroke::Solid(3.0)), color: ink }, |
| 26 | Rounded { rect: [140.0, 100.0, 260.0, 160.0], radius: [16.0, 16.0], stroke: Some((2.0, true)), colors: [ink; 2] }, | 29 | RoundedRect { rect: [140.0, 100.0, 260.0, 160.0], radius: [16.0, 16.0], stroke: Some(Stroke::Dashed(2.0)), color: ink }, |
| 27 | Rounded { rect: [280.0, 100.0, 380.0, 160.0], radius: [8.0, 8.0], stroke: None, colors: [[0.9, 0.5, 0.1, 1.0], [0.2, 0.05, 0.4, 1.0]] }, | 30 | Gradient { rect: [280.0, 100.0, 380.0, 160.0], radius: [8.0, 8.0], colors: [[0.9, 0.5, 0.1, 1.0], [0.2, 0.05, 0.4, 1.0]] }, |
| 28 | Image { rect: [20.0, 180.0, 120.0, 280.0] }, | 31 | Image { image: checker, rect: [20.0, 180.0, 120.0, 280.0] }, |
| 29 | Rect { rect: [60.0, 200.0, 200.0, 260.0], color: [0.1, 0.3, 0.8, 0.5] }, | 32 | Rect { rect: [60.0, 200.0, 200.0, 260.0], color: [0.1, 0.3, 0.8, 0.5] }, |
| 30 | Rounded { rect: [220.0, 180.0, 380.0, 280.0], radius: [30.0, 10.0], stroke: Some((3.0, true)), colors: [red; 2] }, | 33 | RoundedRect { rect: [220.0, 180.0, 380.0, 280.0], radius: [30.0, 10.0], stroke: Some(Stroke::Dashed(3.0)), color: red }, |
| 31 | ] | 34 | ] |
| 32 | } | 35 | } |
| 33 | 36 | ||
| 34 | /// A 4×4 opaque sRGB checker, stretched and filtered by the image path. | 37 | pub fn layer<'a>(primitives: &'a [Primitive<'a>]) -> Layer<'a> { |
| 35 | pub fn checker() -> ([u32; 2], Vec<u8>) { | 38 | Layer { scale: 1.0, origin: [0.0; 2], clip: None, backdrop: None, motion: None, primitives } |
| 36 | let pixels = (0..16) | 39 | } |
| 37 | .flat_map(|i| if (i % 4 + i / 4) % 2 == 0 { [240, 200, 40, 255] } else { [30, 120, 90, 255] }) | 40 | |
| 38 | .collect(); | 41 | /// Lines of interface text through `ui`: black on white like a Cocoa label, the same in |
| 39 | ([4, 4], pixels) | 42 | /// Arial, then the light theme's text on its strip. Logical pixels. |
| 43 | pub const TEXT_SIZE: [u32; 2] = [640, 150]; | ||
| 44 | pub const TEXT: [&str; 2] = [ | ||
| 45 | "Garden Kitchen Journal Reading Trips Home", | ||
| 46 | "Search All Notebooks Up to date 100% Calibri", | ||
| 47 | ]; | ||
| 48 | |||
| 49 | pub fn interface(scale: f32) -> ui::Ui { | ||
| 50 | let theme = ui::Theme::light(); | ||
| 51 | let strip = theme.strip; | ||
| 52 | let mut ui = ui::Ui::new(theme, std::time::Duration::from_millis(500)); | ||
| 53 | ui.begin(TEXT_SIZE.map(|side| side as f32), scale, std::time::Instant::now()); | ||
| 54 | ui.open("page", ui::Spec { size: [ui::fill(), ui::fill()], axis: ui::Axis::Y, fill: Some([1.0; 4]), pad: [10.0, 6.0], gap: 6.0, ..Default::default() }); | ||
| 55 | let black = Some([0.0, 0.0, 0.0, 1.0]); | ||
| 56 | for (index, text) in TEXT.into_iter().enumerate() { | ||
| 57 | ui.leaf(index, ui::Spec { size: [ui::fit(), ui::px(24.0)], text: Some(text), color: black, ..Default::default() }); | ||
| 58 | } | ||
| 59 | ui.leaf("arial", ui::Spec { size: [ui::fit(), ui::px(24.0)], text: Some(TEXT[0]), font: Some("Arial"), color: black, ..Default::default() }); | ||
| 60 | ui.leaf("strip", ui::Spec { size: [ui::fill(), ui::px(24.0)], text: Some(TEXT[1]), fill: Some(strip), ..Default::default() }); | ||
| 61 | ui.close(); | ||
| 62 | ui.end(); | ||
| 63 | ui | ||
| 64 | } | ||
| 65 | |||
| 66 | pub fn interface_layers<'a>(layers: &'a [ui::Layer<'a>], scale: f32) -> Vec<Layer<'a>> { | ||
| 67 | layers | ||
| 68 | .iter() | ||
| 69 | .map(|layer| match layer { | ||
| 70 | ui::Layer::Primitives(primitives) => primitives.layer(scale), | ||
| 71 | ui::Layer::Custom { .. } => unreachable!(), | ||
| 72 | }) | ||
| 73 | .collect() | ||
| 40 | } | 74 | } |
platform/snow-leopard/probe/src/text.rs created+20| ... | @@ -0,0 +1,20 @@ | ||
| 1 | //! Paints `scene::interface` through `draw` over OpenGL and writes `text-probe.png`, to | ||
| 2 | //! set beside a Cocoa label's pixels. | ||
| 3 | #![allow(deprecated)] // OpenGL is the only GPU API 10.6 has. | ||
| 4 | mod context; | ||
| 5 | mod scene; | ||
| 6 | |||
| 7 | fn main() { | ||
| 8 | objc2::rc::autoreleasepool(|_| { | ||
| 9 | let _context = context::current(); | ||
| 10 | let mut renderer = draw::Renderer::new().unwrap_or_else(|error| panic!("{error}")); | ||
| 11 | let target = draw::Target::new(scene::TEXT_SIZE).unwrap(); | ||
| 12 | let ui = scene::interface(1.0); | ||
| 13 | let layers = ui.layers(); | ||
| 14 | renderer.draw(&target, scene::TEXT_SIZE, [1.0; 4], &scene::interface_layers(&layers, 1.0)).unwrap(); | ||
| 15 | let file = std::fs::File::create("text-probe.png").unwrap(); | ||
| 16 | let mut encoder = png::Encoder::new(file, scene::TEXT_SIZE[0], scene::TEXT_SIZE[1]); | ||
| 17 | encoder.set_color(png::ColorType::Rgba); | ||
| 18 | encoder.write_header().unwrap().write_image_data(&target.read_pixels()).unwrap(); | ||
| 19 | }); | ||
| 20 | } | ||
platform/snow-leopard/probe/src/window.rs deleted-133| ... | @@ -1,133 +0,0 @@ | ||
| 1 | //! Opens a window with an OpenGL 2.1 view, paints a test pattern through an sRGB | ||
| 2 | //! framebuffer object, presents it, and writes the framebuffer to `window-probe.png`. | ||
| 3 | #![allow(deprecated)] // OpenGL is the only GPU API 10.6 has. | ||
| 4 | use gl21::*; | ||
| 5 | use objc2::{MainThreadMarker, rc::Retained}; | ||
| 6 | use objc2_app_kit::{ | ||
| 7 | NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSOpenGLPFAAccelerated, | ||
| 8 | NSOpenGLPFAAlphaSize, NSOpenGLPFAColorSize, NSOpenGLPFADoubleBuffer, NSOpenGLPixelFormat, | ||
| 9 | NSOpenGLView, NSWindow, NSWindowStyleMask, | ||
| 10 | }; | ||
| 11 | use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSPoint, NSRect, NSSize, NSString}; | ||
| 12 | |||
| 13 | const SIZE: [i32; 2] = [480, 320]; | ||
| 14 | |||
| 15 | fn main() { | ||
| 16 | // 10.6 has no implicit pool on the main thread; AppKit autoreleases from the start. | ||
| 17 | objc2::rc::autoreleasepool(|_| run()); | ||
| 18 | } | ||
| 19 | |||
| 20 | fn run() { | ||
| 21 | let mtm = MainThreadMarker::new().unwrap(); | ||
| 22 | let app = NSApplication::sharedApplication(mtm); | ||
| 23 | app.setActivationPolicy(NSApplicationActivationPolicy::Regular); | ||
| 24 | app.finishLaunching(); | ||
| 25 | |||
| 26 | let frame = NSRect::new(NSPoint::new(200.0, 200.0), NSSize::new(SIZE[0] as f64, SIZE[1] as f64)); | ||
| 27 | let window = unsafe { | ||
| 28 | NSWindow::initWithContentRect_styleMask_backing_defer( | ||
| 29 | mtm.alloc(), | ||
| 30 | frame, | ||
| 31 | NSWindowStyleMask::Titled | NSWindowStyleMask::Closable | NSWindowStyleMask::Resizable, | ||
| 32 | NSBackingStoreType::Buffered, | ||
| 33 | false, | ||
| 34 | ) | ||
| 35 | }; | ||
| 36 | unsafe { window.setReleasedWhenClosed(false) }; | ||
| 37 | window.setTitle(&NSString::from_str("Snowbound on Snow Leopard")); | ||
| 38 | let attributes = [ | ||
| 39 | NSOpenGLPFADoubleBuffer, | ||
| 40 | NSOpenGLPFAAccelerated, | ||
| 41 | NSOpenGLPFAColorSize, | ||
| 42 | 24, | ||
| 43 | NSOpenGLPFAAlphaSize, | ||
| 44 | 8, | ||
| 45 | 0, | ||
| 46 | ]; | ||
| 47 | let format: Retained<NSOpenGLPixelFormat> = unsafe { | ||
| 48 | NSOpenGLPixelFormat::initWithAttributes( | ||
| 49 | mtm.alloc(), | ||
| 50 | std::ptr::NonNull::new(attributes.as_ptr().cast_mut()).unwrap(), | ||
| 51 | ) | ||
| 52 | } | ||
| 53 | .expect("pixel format"); | ||
| 54 | let view = NSOpenGLView::initWithFrame_pixelFormat(mtm.alloc(), frame, Some(&format)).unwrap(); | ||
| 55 | window.setContentView(Some(&view)); | ||
| 56 | window.makeKeyAndOrderFront(None); | ||
| 57 | app.activateIgnoringOtherApps(true); | ||
| 58 | |||
| 59 | // Let the window server map the window before painting into it. | ||
| 60 | let until = NSDate::dateWithTimeIntervalSinceNow(0.5); | ||
| 61 | while let Some(event) = unsafe { | ||
| 62 | app.nextEventMatchingMask_untilDate_inMode_dequeue( | ||
| 63 | objc2_app_kit::NSEventMask::Any, | ||
| 64 | Some(&until), | ||
| 65 | NSDefaultRunLoopMode, | ||
| 66 | true, | ||
| 67 | ) | ||
| 68 | } { | ||
| 69 | app.sendEvent(&event); | ||
| 70 | } | ||
| 71 | |||
| 72 | let context = view.openGLContext().expect("context"); | ||
| 73 | context.makeCurrentContext(); | ||
| 74 | println!("GL_VENDOR {}", string(VENDOR)); | ||
| 75 | println!("GL_RENDERER {}", string(RENDERER)); | ||
| 76 | println!("GL_VERSION {}", string(VERSION)); | ||
| 77 | let extensions = string(EXTENSIONS); | ||
| 78 | for wanted in ["GL_EXT_framebuffer_object", "GL_EXT_framebuffer_blit", "GL_EXT_framebuffer_sRGB", "GL_EXT_texture_sRGB", "GL_ARB_texture_non_power_of_two"] { | ||
| 79 | println!("{wanted:32} {}", extensions.split(' ').any(|e| e == wanted)); | ||
| 80 | } | ||
| 81 | |||
| 82 | let pixels = unsafe { paint() }; | ||
| 83 | unsafe { | ||
| 84 | glBindFramebufferEXT(DRAW_FRAMEBUFFER, 0); | ||
| 85 | glBlitFramebufferEXT(0, 0, SIZE[0], SIZE[1], 0, 0, SIZE[0], SIZE[1], COLOR_BUFFER_BIT, NEAREST as GLenum); | ||
| 86 | } | ||
| 87 | context.flushBuffer(); | ||
| 88 | println!("GL error after present: {:#x}", unsafe { glGetError() }); | ||
| 89 | |||
| 90 | let file = std::fs::File::create("window-probe.png").unwrap(); | ||
| 91 | let mut encoder = png::Encoder::new(file, SIZE[0] as u32, SIZE[1] as u32); | ||
| 92 | encoder.set_color(png::ColorType::Rgba); | ||
| 93 | encoder.write_header().unwrap().write_image_data(&pixels).unwrap(); | ||
| 94 | println!("wrote window-probe.png"); | ||
| 95 | } | ||
| 96 | |||
| 97 | /// Clears an sRGB framebuffer object and fills three bands with scissored clears in | ||
| 98 | /// linear colours; returns its pixels top row first. | ||
| 99 | unsafe fn paint() -> Vec<u8> { | ||
| 100 | unsafe { | ||
| 101 | let mut texture = 0; | ||
| 102 | glGenTextures(1, &mut texture); | ||
| 103 | glBindTexture(TEXTURE_2D, texture); | ||
| 104 | glTexParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST); | ||
| 105 | glTexParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST); | ||
| 106 | glTexImage2D(TEXTURE_2D, 0, SRGB8_ALPHA8, SIZE[0], SIZE[1], 0, RGBA, UNSIGNED_BYTE, std::ptr::null()); | ||
| 107 | let mut framebuffer = 0; | ||
| 108 | glGenFramebuffersEXT(1, &mut framebuffer); | ||
| 109 | glBindFramebufferEXT(FRAMEBUFFER, framebuffer); | ||
| 110 | glFramebufferTexture2DEXT(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, texture, 0); | ||
| 111 | assert_eq!(glCheckFramebufferStatusEXT(FRAMEBUFFER), FRAMEBUFFER_COMPLETE); | ||
| 112 | glEnable(FRAMEBUFFER_SRGB); | ||
| 113 | glViewport(0, 0, SIZE[0], SIZE[1]); | ||
| 114 | glClearColor(0.9, 0.9, 0.9, 1.0); | ||
| 115 | glClear(COLOR_BUFFER_BIT); | ||
| 116 | glEnable(SCISSOR_TEST); | ||
| 117 | // Linear 0.214 is sRGB 128: the middle band reads back as 128 if encoding works. | ||
| 118 | for (i, color) in [[0.8, 0.1, 0.1], [0.214, 0.214, 0.214], [0.1, 0.2, 0.8]].iter().enumerate() { | ||
| 119 | glScissor(40 + i as i32 * 140, 60, 120, 200); | ||
| 120 | glClearColor(color[0], color[1], color[2], 1.0); | ||
| 121 | glClear(COLOR_BUFFER_BIT); | ||
| 122 | } | ||
| 123 | glDisable(SCISSOR_TEST); | ||
| 124 | let mut pixels = vec![0u8; (SIZE[0] * SIZE[1] * 4) as usize]; | ||
| 125 | glReadPixels(0, 0, SIZE[0], SIZE[1], RGBA, UNSIGNED_BYTE, pixels.as_mut_ptr().cast()); | ||
| 126 | glBindFramebufferEXT(READ_FRAMEBUFFER, framebuffer); | ||
| 127 | let row = (SIZE[0] * 4) as usize; | ||
| 128 | let flipped: Vec<u8> = pixels.chunks(row).rev().flatten().copied().collect(); | ||
| 129 | let middle = &flipped[(160 * SIZE[0] as usize + 250) * 4..][..4]; | ||
| 130 | println!("middle band reads {middle:?} (128 means sRGB encode on write)"); | ||
| 131 | flipped | ||
| 132 | } | ||
| 133 | } | ||
platform/snow-leopard/reference/Cargo.toml+1| ... | @@ -10,3 +10,4 @@ draw = { path = "../../../crates/draw" } | ... | @@ -10,3 +10,4 @@ draw = { path = "../../../crates/draw" } |
| 10 | wgpu = "30.0.1" | 10 | wgpu = "30.0.1" |
| 11 | pollster = "0.4" | 11 | pollster = "0.4" |
| 12 | png = "0.18" | 12 | png = "0.18" |
| 13 | ui = { path = "../../../crates/ui" } |
platform/snow-leopard/reference/src/main.rs+20-31| ... | @@ -1,18 +1,26 @@ | ... | @@ -1,18 +1,26 @@ |
| 1 | //! `reference OUT.png` paints the render-probe scene through `draw::Renderer` over wgpu; | 1 | //! `reference OUT.png` paints the render-probe scene through `draw` over wgpu, |
| 2 | //! `reference --compare A.png B.png DIFF.png` reports how two renders differ and writes | 2 | //! `reference --text SCALE OUT.png` the text-probe scene; `reference --compare A.png B.png |
| 3 | //! their difference, amplified. | 3 | //! DIFF.png` reports how two renders differ and writes their difference, amplified. |
| 4 | #[path = "../../probe/src/scene.rs"] | 4 | #[path = "../../probe/src/scene.rs"] |
| 5 | mod scene; | 5 | mod scene; |
| 6 | 6 | ||
| 7 | use draw::{Layer, Primitive, RasterImage, Stroke}; | ||
| 8 | use scene::Shape; | ||
| 9 | |||
| 10 | fn main() { | 7 | fn main() { |
| 11 | let args: Vec<String> = std::env::args().skip(1).collect(); | 8 | let args: Vec<String> = std::env::args().skip(1).collect(); |
| 12 | match args.as_slice() { | 9 | match args.as_slice() { |
| 13 | [flag, a, b, diff] if flag == "--compare" => compare(a, b, diff), | 10 | [flag, a, b, diff] if flag == "--compare" => compare(a, b, diff), |
| 14 | [out] => paint(out), | 11 | [flag, scale, out] if flag == "--text" => { |
| 15 | _ => panic!("usage: reference OUT.png | reference --compare A.png B.png DIFF.png"), | 12 | let scale: f32 = scale.parse().unwrap(); |
| 13 | let ui = scene::interface(scale); | ||
| 14 | let layers = ui.layers(); | ||
| 15 | let size = scene::TEXT_SIZE.map(|side| (side as f32 * scale) as u32); | ||
| 16 | paint(out, size, [1.0; 4], &scene::interface_layers(&layers, scale)); | ||
| 17 | } | ||
| 18 | [out] => { | ||
| 19 | let checker = scene::checker(); | ||
| 20 | let primitives = scene::primitives(&checker); | ||
| 21 | paint(out, scene::SIZE, scene::CLEAR, &[scene::layer(&primitives)]); | ||
| 22 | } | ||
| 23 | _ => panic!("usage: reference [--text SCALE] OUT.png | reference --compare A.png B.png DIFF.png"), | ||
| 16 | } | 24 | } |
| 17 | } | 25 | } |
| 18 | 26 | ||
| ... | @@ -47,31 +55,13 @@ fn write(path: &str, [width, height]: [u32; 2], pixels: &[u8]) { | ... | @@ -47,31 +55,13 @@ fn write(path: &str, [width, height]: [u32; 2], pixels: &[u8]) { |
| 47 | encoder.write_header().unwrap().write_image_data(pixels).unwrap(); | 55 | encoder.write_header().unwrap().write_image_data(pixels).unwrap(); |
| 48 | } | 56 | } |
| 49 | 57 | ||
| 50 | fn paint(out: &str) { | 58 | fn paint(out: &str, size: [u32; 2], clear: [f32; 4], layers: &[draw::Layer<'_>]) { |
| 51 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | 59 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); |
| 52 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | 60 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); |
| 53 | let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | 61 | let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap(); |
| 54 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; | 62 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; |
| 55 | let mut renderer = draw::Renderer::new(device.clone(), queue.clone(), format); | 63 | let mut renderer = draw::Renderer::new(device.clone(), queue.clone(), format); |
| 56 | let (checker_size, checker) = scene::checker(); | 64 | let [width, height] = size; |
| 57 | let image = RasterImage::new(checker_size, checker).unwrap(); | ||
| 58 | let primitives: Vec<Primitive> = scene::shapes() | ||
| 59 | .into_iter() | ||
| 60 | .map(|shape| match shape { | ||
| 61 | Shape::Rect { rect, color } => Primitive::Rect { rect, color }, | ||
| 62 | Shape::Rounded { rect, radius, stroke, colors } => match stroke { | ||
| 63 | None => Primitive::Gradient { rect, radius, colors }, | ||
| 64 | Some((width, dashed)) => Primitive::RoundedRect { | ||
| 65 | rect, | ||
| 66 | radius, | ||
| 67 | stroke: Some(if dashed { Stroke::Dashed(width) } else { Stroke::Solid(width) }), | ||
| 68 | color: colors[0], | ||
| 69 | }, | ||
| 70 | }, | ||
| 71 | Shape::Image { rect } => Primitive::Image { image: &image, rect }, | ||
| 72 | }) | ||
| 73 | .collect(); | ||
| 74 | let [width, height] = scene::SIZE; | ||
| 75 | let texture = device.create_texture(&wgpu::TextureDescriptor { | 65 | let texture = device.create_texture(&wgpu::TextureDescriptor { |
| 76 | label: None, | 66 | label: None, |
| 77 | size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, | 67 | size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 }, |
| ... | @@ -82,9 +72,8 @@ fn paint(out: &str) { | ... | @@ -82,9 +72,8 @@ fn paint(out: &str) { |
| 82 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, | 72 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, |
| 83 | view_formats: &[], | 73 | view_formats: &[], |
| 84 | }); | 74 | }); |
| 85 | let layer = Layer { scale: 1.0, origin: [0.0; 2], clip: None, backdrop: None, primitives: &primitives }; | ||
| 86 | renderer | 75 | renderer |
| 87 | .draw(&texture.create_view(&Default::default()), scene::SIZE, scene::CLEAR, &[layer]) | 76 | .draw(&texture.create_view(&Default::default()), size, clear, layers) |
| 88 | .unwrap(); | 77 | .unwrap(); |
| 89 | let row = (width * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); | 78 | let row = (width * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); |
| 90 | let buffer = device.create_buffer(&wgpu::BufferDescriptor { | 79 | let buffer = device.create_buffer(&wgpu::BufferDescriptor { |
| ... | @@ -107,5 +96,5 @@ fn paint(out: &str) { | ... | @@ -107,5 +96,5 @@ fn paint(out: &str) { |
| 107 | device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None }).unwrap(); | 96 | device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None }).unwrap(); |
| 108 | let mapped = buffer.get_mapped_range(..).unwrap(); | 97 | let mapped = buffer.get_mapped_range(..).unwrap(); |
| 109 | let pixels: Vec<u8> = mapped.chunks(row as usize).flat_map(|r| &r[..width as usize * 4]).copied().collect(); | 98 | let pixels: Vec<u8> = mapped.chunks(row as usize).flat_map(|r| &r[..width as usize * 4]).copied().collect(); |
| 110 | write(out, scene::SIZE, &pixels); | 99 | write(out, size, &pixels); |
| 111 | } | 100 | } |
platform/snow-leopard/remote.sh+69-4| ... | @@ -4,6 +4,14 @@ | ... | @@ -4,6 +4,14 @@ |
| 4 | # remote.sh run [-C DIR] BINARY [ARGS...] | 4 | # remote.sh run [-C DIR] BINARY [ARGS...] |
| 5 | # ship to /tmp/snow-leopard, run in DIR (default there), | 5 | # ship to /tmp/snow-leopard, run in DIR (default there), |
| 6 | # print any new crash log | 6 | # print any new crash log |
| 7 | # remote.sh start BINARY [ARGS...] ship and leave running there, output in NAME.log; | ||
| 8 | # BINARY may be an .app bundle | ||
| 9 | # remote.sh stop NAME end what start started (a bundle by its executable) | ||
| 10 | # remote.sh screen LOCAL.png wake the display and capture it (screencapture | ||
| 11 | # writes nothing from ssh) | ||
| 12 | # remote.sh input ARGS... click X Y | type TEXT | key KEYCODE [command] | ||
| 13 | # remote.sh selectors BINARY selector-like names BINARY holds that nothing on | ||
| 14 | # 10.6 implements | ||
| 7 | # remote.sh mirror PATH... copy repo paths to the same absolute paths there, so | 15 | # remote.sh mirror PATH... copy repo paths to the same absolute paths there, so |
| 8 | # test binaries find fixtures by CARGO_MANIFEST_DIR | 16 | # test binaries find fixtures by CARGO_MANIFEST_DIR |
| 9 | # remote.sh pull REMOTE [LOCAL] copy a file back (e.g. a screenshot) | 17 | # remote.sh pull REMOTE [LOCAL] copy a file back (e.g. a screenshot) |
| ... | @@ -14,6 +22,33 @@ host=${SNOW_LEOPARD_HOST:-osx10.6@10.0.0.1} | ... | @@ -14,6 +22,33 @@ host=${SNOW_LEOPARD_HOST:-osx10.6@10.0.0.1} |
| 14 | here=$(cd "$(dirname "$0")" && pwd) | 22 | here=$(cd "$(dirname "$0")" && pwd) |
| 15 | dir=/tmp/snow-leopard | 23 | dir=/tmp/snow-leopard |
| 16 | 24 | ||
| 25 | ship() { | ||
| 26 | base=$(basename "$1") | ||
| 27 | if [ -d "$1" ]; then | ||
| 28 | (cd "$(dirname "$1")" && COPYFILE_DISABLE=1 tar czf - --no-mac-metadata --no-xattrs "$base") | | ||
| 29 | ssh "$host" "mkdir -p $dir && rm -rf '$dir/${base:?}' && tar xzf - -C $dir" | ||
| 30 | else | ||
| 31 | ssh "$host" "mkdir -p $dir && cat > $dir/$base && chmod +x $dir/$base" < "$1" | ||
| 32 | fi | ||
| 33 | } | ||
| 34 | |||
| 35 | # Builds a console/ tool against the 10.6 SDK and ships it. | ||
| 36 | tool() { | ||
| 37 | sdk=$here/../../target/snow-leopard/MacOSX10.6.sdk | ||
| 38 | out=$here/../../target/snow-leopard/$1 | ||
| 39 | if [ ! "$out" -nt "$here/console/$1.c" ]; then | ||
| 40 | clang -arch x86_64 -isysroot "$sdk" -mmacosx-version-min=10.6 -O2 -Wall \ | ||
| 41 | -framework ApplicationServices -framework CoreServices -lobjc \ | ||
| 42 | "$here/console/$1.c" -o "$out" 2>&1 | grep -v -e "MH_DYLIB_STUB" -e "no platform load" >&2 || true | ||
| 43 | fi | ||
| 44 | ship "$out" | ||
| 45 | } | ||
| 46 | |||
| 47 | quote() { | ||
| 48 | quoted= | ||
| 49 | for arg; do quoted="$quoted '$(printf %s "$arg" | sed "s/'/'\\\\''/g")'"; done | ||
| 50 | } | ||
| 51 | |||
| 17 | case "${1:-}" in | 52 | case "${1:-}" in |
| 18 | run) | 53 | run) |
| 19 | shift | 54 | shift |
| ... | @@ -22,9 +57,8 @@ run) | ... | @@ -22,9 +57,8 @@ run) |
| 22 | binary=$1 | 57 | binary=$1 |
| 23 | shift | 58 | shift |
| 24 | name=$(basename "$binary") | 59 | name=$(basename "$binary") |
| 25 | ssh "$host" "mkdir -p $dir && cat > $dir/$name && chmod +x $dir/$name" < "$binary" | 60 | ship "$binary" |
| 26 | quoted= | 61 | quote "$@" |
| 27 | for arg; do quoted="$quoted '$(printf %s "$arg" | sed "s/'/'\\\\''/g")'"; done | ||
| 28 | # A crash report lands a few seconds after the process dies. | 62 | # A crash report lands a few seconds after the process dies. |
| 29 | ssh "$host" "touch $dir/.start && cd '$cwd' && status=0 && $dir/$name$quoted || status=\$? | 63 | ssh "$host" "touch $dir/.start && cd '$cwd' && status=0 && $dir/$name$quoted || status=\$? |
| 30 | if [ \$status -ge 128 ]; then | 64 | if [ \$status -ge 128 ]; then |
| ... | @@ -35,6 +69,37 @@ run) | ... | @@ -35,6 +69,37 @@ run) |
| 35 | fi | 69 | fi |
| 36 | exit \$status" < /dev/null | 70 | exit \$status" < /dev/null |
| 37 | ;; | 71 | ;; |
| 72 | start) | ||
| 73 | shift | ||
| 74 | name=$(basename "$1") | ||
| 75 | program=./$name | ||
| 76 | if [ -d "$1" ]; then | ||
| 77 | name=$(/usr/libexec/PlistBuddy -c 'Print CFBundleExecutable' "$1/Contents/Info.plist") | ||
| 78 | program=./$(basename "$1")/Contents/MacOS/$name | ||
| 79 | fi | ||
| 80 | ship "$1" | ||
| 81 | shift | ||
| 82 | quote "$@" | ||
| 83 | ssh "$host" "cd $dir && (nohup $program$quoted < /dev/null > $name.log 2>&1 &)" | ||
| 84 | ;; | ||
| 85 | stop) | ||
| 86 | ssh "$host" "kill \$(ps axo pid,command | awk '{ sub(\".*/\", \"\", \$2) } \$2 == \"$2\" { print \$1 }')" | ||
| 87 | ;; | ||
| 88 | screen) | ||
| 89 | tool screen | ||
| 90 | ssh "$host" "$dir/screen $dir/screen.png && cat $dir/screen.png" > "$2" | ||
| 91 | ;; | ||
| 92 | input) | ||
| 93 | shift | ||
| 94 | tool input | ||
| 95 | quote "$@" | ||
| 96 | ssh "$host" "$dir/input$quoted" | ||
| 97 | ;; | ||
| 98 | selectors) | ||
| 99 | tool selectors | ||
| 100 | strings -a "$2" | grep -E '^[a-z][a-z]+([A-Z][a-z0-9]+)*[A-Z]?[A-Za-z0-9]*(:([A-Za-z][A-Za-z0-9]*:)*)?$' | | ||
| 101 | grep -E '[a-z]{2}[A-Z][a-z]|:$' | sort -u | ssh "$host" "$dir/selectors 2>/dev/null" | ||
| 102 | ;; | ||
| 38 | mirror) | 103 | mirror) |
| 39 | shift | 104 | shift |
| 40 | root=$(cd "$here/../.." && pwd) | 105 | root=$(cd "$here/../.." && pwd) |
| ... | @@ -54,7 +119,7 @@ sh) | ... | @@ -54,7 +119,7 @@ sh) |
| 54 | ssh "$host" "$@" | 119 | ssh "$host" "$@" |
| 55 | ;; | 120 | ;; |
| 56 | *) | 121 | *) |
| 57 | sed -n '2,12s/^# \{0,1\}//p' "$0" >&2 | 122 | sed -n '2,20s/^# \{0,1\}//p' "$0" >&2 |
| 58 | exit 2 | 123 | exit 2 |
| 59 | ;; | 124 | ;; |
| 60 | esac | 125 | esac |
platform/snow-leopard/rt/appkit.c created+146| ... | @@ -0,0 +1,146 @@ | ||
| 1 | // AppKit that winit, AccessKit and the app call and 10.6 lacks, added at load time with | ||
| 2 | // the neutral answer: scale 1, no precise scrolling, no appearances, no tabbing, no full | ||
| 3 | // screen. `remote.sh selectors` lists what a binary names that 10.6 lacks. Linked from | ||
| 4 | // the archive by the appearance name winit imports, so only AppKit binaries carry it. | ||
| 5 | // class_addMethod adds nothing where the system already has the method. | ||
| 6 | #include <ApplicationServices/ApplicationServices.h> | ||
| 7 | #include <objc/message.h> | ||
| 8 | #include <objc/runtime.h> | ||
| 9 | |||
| 10 | // 10.9's appearance name; it names the stub appearance below. | ||
| 11 | const CFStringRef NSAppearanceNameAqua = CFSTR("NSAppearanceNameAqua"); | ||
| 12 | |||
| 13 | // Accessibility names from 10.7 to 10.13, which AccessKit imports. 10.6's VoiceOver | ||
| 14 | // asks through the older attribute protocol, which never sees them. | ||
| 15 | const CFStringRef NSAccessibilityAnnouncementKey = CFSTR("AXAnnouncementKey"); | ||
| 16 | const CFStringRef NSAccessibilityAnnouncementRequestedNotification = CFSTR("AXAnnouncementRequested"); | ||
| 17 | const CFStringRef NSAccessibilityLanguageTextAttribute = CFSTR("AXLanguageText"); | ||
| 18 | const CFStringRef NSAccessibilityPriorityKey = CFSTR("AXPriorityKey"); | ||
| 19 | const CFStringRef NSAccessibilitySwitchSubrole = CFSTR("AXSwitch"); | ||
| 20 | const CFStringRef NSAccessibilityTabButtonSubrole = CFSTR("AXTabButton"); | ||
| 21 | const CFStringRef NSAccessibilityTextAlignmentAttribute = CFSTR("AXTextAlignment"); | ||
| 22 | const CFStringRef NSAccessibilityToggleSubrole = CFSTR("AXToggle"); | ||
| 23 | |||
| 24 | extern void NSAccessibilityPostNotification(id element, CFStringRef notification); | ||
| 25 | |||
| 26 | void NSAccessibilityPostNotificationWithUserInfo(id element, CFStringRef notification, id info) { | ||
| 27 | NSAccessibilityPostNotification(element, notification); | ||
| 28 | } | ||
| 29 | |||
| 30 | typedef id (*Send)(id, SEL); | ||
| 31 | #define SEND(receiver, selector) ((Send)objc_msgSend)((id)(receiver), sel_registerName(selector)) | ||
| 32 | typedef double (*SendDouble)(id, SEL); | ||
| 33 | |||
| 34 | static double one(id self, SEL _cmd) { return 1.0; } | ||
| 35 | static void ignore(id self, SEL _cmd, id value) {} | ||
| 36 | static BOOL no(id self, SEL _cmd) { return NO; } | ||
| 37 | static long zero(id self, SEL _cmd) { return 0; } | ||
| 38 | static id nothing(id self, SEL _cmd) { return nil; } | ||
| 39 | |||
| 40 | static double delta_x(id self, SEL _cmd) { return ((SendDouble)objc_msgSend)(self, sel_registerName("deltaX")); } | ||
| 41 | static double delta_y(id self, SEL _cmd) { return ((SendDouble)objc_msgSend)(self, sel_registerName("deltaY")); } | ||
| 42 | |||
| 43 | // 10.7: the window's frame origin is where its base coordinates start. | ||
| 44 | static CGRect rect_to_screen(id self, SEL _cmd, CGRect rect) { | ||
| 45 | CGRect frame = ((CGRect(*)(id, SEL))objc_msgSend_stret)(self, sel_registerName("frame")); | ||
| 46 | rect.origin.x += frame.origin.x; | ||
| 47 | rect.origin.y += frame.origin.y; | ||
| 48 | return rect; | ||
| 49 | } | ||
| 50 | |||
| 51 | static CGPoint point_from_screen(id self, SEL _cmd, CGPoint point) { | ||
| 52 | CGRect frame = ((CGRect(*)(id, SEL))objc_msgSend_stret)(self, sel_registerName("frame")); | ||
| 53 | return CGPointMake(point.x - frame.origin.x, point.y - frame.origin.y); | ||
| 54 | } | ||
| 55 | |||
| 56 | static id srgb_color(id self, SEL _cmd, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) { | ||
| 57 | CGFloat components[] = {red, green, blue, alpha}; | ||
| 58 | return ((id (*)(id, SEL, id, CGFloat *, long))objc_msgSend)( | ||
| 59 | self, sel_registerName("colorWithColorSpace:components:count:"), | ||
| 60 | SEND(objc_getClass("NSColorSpace"), "sRGBColorSpace"), components, 4); | ||
| 61 | } | ||
| 62 | |||
| 63 | // 10.10's label is what 10.6 calls the description attribute. | ||
| 64 | static void set_accessibility_label(id self, SEL _cmd, id label) { | ||
| 65 | ((void (*)(id, SEL, id, CFStringRef))objc_msgSend)( | ||
| 66 | self, sel_registerName("accessibilitySetOverrideValue:forAttribute:"), label, CFSTR("AXDescription")); | ||
| 67 | } | ||
| 68 | |||
| 69 | static id appearance_class; | ||
| 70 | |||
| 71 | static id stub_appearance(id self, SEL _cmd) { | ||
| 72 | static id shared; | ||
| 73 | if (!shared) shared = SEND(SEND(appearance_class, "alloc"), "init"); | ||
| 74 | return shared; | ||
| 75 | } | ||
| 76 | |||
| 77 | static id aqua(id self, SEL _cmd) { return (id)NSAppearanceNameAqua; } | ||
| 78 | static id best_match(id self, SEL _cmd, id names) { return (id)NSAppearanceNameAqua; } | ||
| 79 | |||
| 80 | // 10.6 answers component accessors only for the named RGB spaces; later systems answer | ||
| 81 | // them for any RGB colour space, such as the sRGB the app converts system colours to. | ||
| 82 | static CGFloat component(id self, int index) { | ||
| 83 | CGFloat components[4] = {0}; | ||
| 84 | ((void (*)(id, SEL, CGFloat *))objc_msgSend)(self, sel_registerName("getComponents:"), components); | ||
| 85 | return components[index]; | ||
| 86 | } | ||
| 87 | static CGFloat red(id self, SEL _cmd) { return component(self, 0); } | ||
| 88 | static CGFloat green(id self, SEL _cmd) { return component(self, 1); } | ||
| 89 | static CGFloat blue(id self, SEL _cmd) { return component(self, 2); } | ||
| 90 | |||
| 91 | static id black(id self, SEL _cmd) { return SEND(objc_getClass("NSColor"), "blackColor"); } | ||
| 92 | static id secondary_selection(id self, SEL _cmd) { | ||
| 93 | return SEND(objc_getClass("NSColor"), "secondarySelectedControlColor"); | ||
| 94 | } | ||
| 95 | |||
| 96 | #define RECT "{CGRect={CGPoint=dd}{CGSize=dd}}" | ||
| 97 | |||
| 98 | static void add(const char *class_name, int meta, const char *selector, void *imp, const char *types) { | ||
| 99 | Class cls = (Class)objc_getClass(class_name); | ||
| 100 | if (meta) cls = object_getClass((id)cls); | ||
| 101 | class_addMethod(cls, sel_registerName(selector), (IMP)imp, types); | ||
| 102 | } | ||
| 103 | |||
| 104 | __attribute__((constructor)) static void polyfill(void) { | ||
| 105 | add("NSScreen", 0, "backingScaleFactor", one, "d@:"); | ||
| 106 | add("NSWindow", 0, "backingScaleFactor", one, "d@:"); | ||
| 107 | add("NSWindow", 0, "convertRectToScreen:", rect_to_screen, RECT "@:" RECT); | ||
| 108 | add("NSWindow", 0, "convertPointFromScreen:", point_from_screen, "{CGPoint=dd}@:{CGPoint=dd}"); | ||
| 109 | add("NSWindow", 0, "performWindowDragWithEvent:", ignore, "v@:@"); | ||
| 110 | add("NSWindow", 0, "toggleFullScreen:", ignore, "v@:@"); | ||
| 111 | add("NSWindow", 0, "setTabbingMode:", ignore, "v@:q"); | ||
| 112 | add("NSWindow", 0, "setTabbingIdentifier:", ignore, "v@:@"); | ||
| 113 | add("NSWindow", 1, "setAllowsAutomaticWindowTabbing:", ignore, "v@:c"); | ||
| 114 | add("NSWindow", 0, "setTitlebarAppearsTransparent:", ignore, "v@:c"); | ||
| 115 | add("NSWindow", 0, "setTitleVisibility:", ignore, "v@:q"); | ||
| 116 | add("NSView", 0, "setWantsBestResolutionOpenGLSurface:", ignore, "v@:c"); | ||
| 117 | add("NSView", 0, "setAccessibilityLabel:", set_accessibility_label, "v@:@"); | ||
| 118 | add("NSDatePicker", 0, "setPresentsCalendarOverlay:", ignore, "v@:c"); | ||
| 119 | add("NSEvent", 0, "hasPreciseScrollingDeltas", no, "c@:"); | ||
| 120 | add("NSEvent", 0, "scrollingDeltaX", delta_x, "d@:"); | ||
| 121 | add("NSEvent", 0, "scrollingDeltaY", delta_y, "d@:"); | ||
| 122 | add("NSEvent", 0, "momentumPhase", zero, "Q@:"); | ||
| 123 | add("NSEvent", 0, "phase", zero, "Q@:"); | ||
| 124 | add("NSColorSpaceColor", 0, "redComponent", red, "d@:"); | ||
| 125 | add("NSColorSpaceColor", 0, "greenComponent", green, "d@:"); | ||
| 126 | add("NSColorSpaceColor", 0, "blueComponent", blue, "d@:"); | ||
| 127 | add("NSColor", 1, "colorWithSRGBRed:green:blue:alpha:", srgb_color, "@@:dddd"); | ||
| 128 | add("NSColor", 1, "textInsertionPointColor", black, "@@:"); | ||
| 129 | add("NSColor", 1, "unemphasizedSelectedTextBackgroundColor", secondary_selection, "@@:"); | ||
| 130 | |||
| 131 | // One appearance, Aqua, which windows have and nothing changes. | ||
| 132 | appearance_class = (id)objc_getClass("NSAppearance"); | ||
| 133 | if (!appearance_class) { | ||
| 134 | Class cls = objc_allocateClassPair((Class)objc_getClass("NSObject"), "NSAppearance", 0); | ||
| 135 | objc_registerClassPair(cls); | ||
| 136 | appearance_class = (id)cls; | ||
| 137 | } | ||
| 138 | add("NSAppearance", 0, "name", aqua, "@@:"); | ||
| 139 | add("NSAppearance", 0, "bestMatchFromAppearancesWithNames:", best_match, "@@:@"); | ||
| 140 | add("NSAppearance", 1, "appearanceNamed:", nothing, "@@:@"); | ||
| 141 | add("NSAppearance", 1, "currentAppearance", nothing, "@@:"); | ||
| 142 | add("NSAppearance", 1, "setCurrentAppearance:", ignore, "v@:@"); | ||
| 143 | add("NSWindow", 0, "appearance", nothing, "@@:"); | ||
| 144 | add("NSWindow", 0, "setAppearance:", ignore, "v@:@"); | ||
| 145 | add("NSWindow", 0, "effectiveAppearance", stub_appearance, "@@:"); | ||
| 146 | } | ||
platform/snow-leopard/rt/objc.c+11| ... | @@ -37,3 +37,14 @@ void *objc_autoreleasePoolPush(void) { | ... | @@ -37,3 +37,14 @@ void *objc_autoreleasePoolPush(void) { |
| 37 | } | 37 | } |
| 38 | 38 | ||
| 39 | void objc_autoreleasePoolPop(void *pool) { SEND((id)pool, "drain"); } | 39 | void objc_autoreleasePoolPop(void *pool) { SEND((id)pool, "drain"); } |
| 40 | |||
| 41 | // 10.6 has no zeroing weak references for non-GC code. These hold the object unretained | ||
| 42 | // and never zero: sound only while the referent outlives the reference, as winit's view | ||
| 43 | // and its window do. | ||
| 44 | id objc_initWeak(id *location, id value) { return *location = value; } | ||
| 45 | id objc_storeWeak(id *location, id value) { return *location = value; } | ||
| 46 | id objc_loadWeakRetained(id *location) { return objc_retain(*location); } | ||
| 47 | id objc_loadWeak(id *location) { return objc_autorelease(objc_loadWeakRetained(location)); } | ||
| 48 | void objc_destroyWeak(id *location) { *location = nil; } | ||
| 49 | void objc_copyWeak(id *to, id *from) { *to = *from; } | ||
| 50 | void objc_moveWeak(id *to, id *from) { *to = *from; *from = nil; } |
tools/canvas/build_macos.py+33-10| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | """Build the local macOS canvas bundle.""" | 2 | """Build the local macOS canvas bundle, or with --snow-leopard the Mac OS X 10.6 one.""" |
| 3 | import argparse | 3 | import argparse |
| 4 | import json | 4 | import json |
| 5 | from pathlib import Path | 5 | from pathlib import Path |
| ... | @@ -7,32 +7,53 @@ import platform | ... | @@ -7,32 +7,53 @@ import platform |
| 7 | import plistlib | 7 | import plistlib |
| 8 | import shutil | 8 | import shutil |
| 9 | import subprocess | 9 | import subprocess |
| 10 | import tempfile | ||
| 10 | 11 | ||
| 11 | parser = argparse.ArgumentParser(description=__doc__) | 12 | parser = argparse.ArgumentParser(description=__doc__) |
| 12 | parser.add_argument('--release', action='store_true') | 13 | parser.add_argument('--release', action='store_true') |
| 13 | parser.add_argument('--output', type=Path, help='Create a separate bundle at a new .app path') | 14 | parser.add_argument('--output', type=Path, help='Create a separate bundle at a new .app path') |
| 14 | parser.add_argument('--bundle-id', help='Bundle identifier for the separate app') | 15 | parser.add_argument('--bundle-id', help='Bundle identifier for the separate app') |
| 16 | parser.add_argument('--snow-leopard', action='store_true', | ||
| 17 | help='Build with platform/snow-leopard/cargo.sh into target/snow-leopard') | ||
| 15 | args = parser.parse_args() | 18 | args = parser.parse_args() |
| 16 | if bool(args.output) != bool(args.bundle_id): | 19 | if bool(args.output) != bool(args.bundle_id): |
| 17 | parser.error('Use --output and --bundle-id together.') | 20 | parser.error('Use --output and --bundle-id together.') |
| 18 | if args.output and (args.output.suffix != '.app' or args.output.exists()): | 21 | if args.output and (args.output.suffix != '.app' or args.output.exists()): |
| 19 | parser.error('Choose a new output path ending in .app.') | 22 | parser.error('Choose a new output path ending in .app.') |
| 20 | root = Path(__file__).resolve().parents[2] | 23 | root = Path(__file__).resolve().parents[2] |
| 21 | subprocess.run(['cargo', 'build', '-p', 'snowbound'] + (['--release'] if args.release else []), cwd=root, check=True) | 24 | profile = ['--release'] if args.release else [] |
| 22 | metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--format-version=1', '--no-deps'], cwd=root)) | 25 | if args.snow_leopard: |
| 23 | target = Path(metadata['target_directory']) | 26 | subprocess.run([root / 'platform/snow-leopard/cargo.sh', 'build', '-p', 'snowbound', '--no-default-features'] + profile, |
| 27 | cwd=root, check=True) | ||
| 28 | target = root / 'target/snow-leopard' | ||
| 29 | built = target / 'x86_64-apple-macosx10.6' | ||
| 30 | else: | ||
| 31 | subprocess.run(['cargo', 'build', '-p', 'snowbound'] + profile, cwd=root, check=True) | ||
| 32 | metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--format-version=1', '--no-deps'], cwd=root)) | ||
| 33 | target = built = Path(metadata['target_directory']) | ||
| 24 | bundle = args.output.resolve() if args.output else target / 'Snowbound.app' | 34 | bundle = args.output.resolve() if args.output else target / 'Snowbound.app' |
| 25 | if args.output: | 35 | if args.output: |
| 26 | bundle.mkdir(parents=True, exist_ok=False) | 36 | bundle.mkdir(parents=True, exist_ok=False) |
| 27 | binary = bundle / 'Contents/MacOS/Snowbound' | 37 | binary = bundle / 'Contents/MacOS/Snowbound' |
| 28 | binary.parent.mkdir(parents=True, exist_ok=True) | 38 | binary.parent.mkdir(parents=True, exist_ok=True) |
| 29 | pending = binary.with_suffix('.next') | 39 | pending = binary.with_suffix('.next') |
| 30 | shutil.copy2(target / ('release' if args.release else 'debug') / 'snowbound', pending) | 40 | shutil.copy2(built / ('release' if args.release else 'debug') / 'snowbound', pending) |
| 31 | pending.replace(binary) | 41 | pending.replace(binary) |
| 32 | icon = 'Snowbound-Tahoe' if int(platform.mac_ver()[0].split('.')[0] or 0) >= 26 else 'Snowbound-Sequoia' | 42 | icons = root / 'crates/snowbound/assets/icon' |
| 33 | resources = bundle / 'Contents/Resources' | 43 | resources = bundle / 'Contents/Resources' |
| 34 | resources.mkdir(exist_ok=True) | 44 | resources.mkdir(exist_ok=True) |
| 35 | shutil.copy2(root / 'crates/snowbound/assets/icon' / f'{icon}.icns', resources / 'Snowbound.icns') | 45 | if args.snow_leopard: |
| 46 | # 10.6 reads 256 and 512 pixel entries (ic08, ic09), which the checked-in icns lacks. | ||
| 47 | with tempfile.TemporaryDirectory() as scratch: | ||
| 48 | iconset = Path(scratch) / 'Snowbound.iconset' | ||
| 49 | iconset.mkdir() | ||
| 50 | for side in [16, 32, 128, 256, 512]: | ||
| 51 | subprocess.run(['sips', '-z', str(side), str(side), icons / 'Snowbound-SnowLeopard.png', | ||
| 52 | '--out', iconset / f'icon_{side}x{side}.png'], check=True, capture_output=True) | ||
| 53 | subprocess.run(['iconutil', '-c', 'icns', iconset, '-o', resources / 'Snowbound.icns'], check=True) | ||
| 54 | else: | ||
| 55 | icon = 'Snowbound-Tahoe' if int(platform.mac_ver()[0].split('.')[0] or 0) >= 26 else 'Snowbound-Sequoia' | ||
| 56 | shutil.copy2(icons / f'{icon}.icns', resources / 'Snowbound.icns') | ||
| 36 | (bundle / 'Contents/Info.plist').write_bytes(plistlib.dumps({ | 57 | (bundle / 'Contents/Info.plist').write_bytes(plistlib.dumps({ |
| 37 | 'CFBundleExecutable': binary.name, | 58 | 'CFBundleExecutable': binary.name, |
| 38 | 'CFBundleIdentifier': args.bundle_id or 'dev.snowbound.app', | 59 | 'CFBundleIdentifier': args.bundle_id or 'dev.snowbound.app', |
| ... | @@ -43,7 +64,9 @@ shutil.copy2(root / 'crates/snowbound/assets/icon' / f'{icon}.icns', resources / | ... | @@ -43,7 +64,9 @@ shutil.copy2(root / 'crates/snowbound/assets/icon' / f'{icon}.icns', resources / |
| 43 | 'NSHighResolutionCapable': True, | 64 | 'NSHighResolutionCapable': True, |
| 44 | 'NSPrincipalClass': 'NSApplication', | 65 | 'NSPrincipalClass': 'NSApplication', |
| 45 | 'CFBundleIconFile': 'Snowbound', | 66 | 'CFBundleIconFile': 'Snowbound', |
| 46 | })) | 67 | } | ({'LSMinimumSystemVersion': '10.6'} if args.snow_leopard else {}))) |
| 47 | subprocess.run(['codesign', '--force', '--sign', '-', str(bundle)], check=True) | 68 | # 10.6 runs the bundle unsigned. |
| 48 | subprocess.run(['codesign', '--verify', '--strict', str(bundle)], check=True) | 69 | if not args.snow_leopard: |
| 70 | subprocess.run(['codesign', '--force', '--sign', '-', str(bundle)], check=True) | ||
| 71 | subprocess.run(['codesign', '--verify', '--strict', str(bundle)], check=True) | ||
| 49 | print(bundle) | 72 | print(bundle) |