| author | |
| committer | |
| log | 1de0431d8bcf7c9e558be7762f0996e76eb899b2 |
| tree | b7f53d59c1576871771688cab411271525fda038 |
| parent | 218d66206d78eb0a122f2a2ab226c0b7a232dcfd |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
- Paste, Paste Options, Cut and Copy are one Clipboard menu at every width.
- The text group runs Styles, font, size, B I U, then Text Effects, highlighter, font colour
and Format Painter on one face with no divider; that tail folds inside the group, which
the ui kit now allows: a row folds groups inside its boxes sized by their children.
- Styles gets an "Aa" icon. A group's trailing "more" arrow is a 14-pt button, so the
group ends as tight as one ending in an icon.
- Page Color follows the drawing tools; the row ends in sync status and the view menu,
which also lists zoom and Page Color.
- Back and Forward stand before the notebook button at the tab row's start and, with the
sidebar open, at its top right. The sidebar reads "My Notebooks", and its button shows a
close cross over the tree's icon column.
- Zoom is a joined − 100% + control left of the search box; clicking the level edits it
(Enter zooms to the percentage, Escape keeps the zoom). It folds away first.
- The toolbar row ends 4 pt short of the 38-pt title bar its buttons centre in, so the tabs
sit closer, on every platform.
- Presses inside a toolbar group, a faded button included, never drag or zoom the window;
on macOS the view tells AppKit it never moves the window, and a double click follows
Desktop & Dock's minimize or zoom setting.
The least window on macOS is 615 pt, from 626.
Assisted-by: claude-opus-5.514 files changed, 561 insertions(+), 291 deletions(-)
arc/platforms.md+6-3| ... | ... | @@ -31,9 +31,12 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. |
| 31 | 31 | ### macOS |
| 32 | 32 | |
| 33 | 33 | - The toolbar's row is the title bar. An empty `NSToolbar`, unified compact |
| 34 | from macOS 11, with the title hidden makes the title bar the row's height, | |
| 35 | and AppKit places the traffic lights in it; a press in the row's gaps drags | |
| 36 | the window. The title is still set, for the Window menu, Mission Control and | |
| 34 | from macOS 11, with the title hidden makes the title bar 38 pt, and AppKit | |
| 35 | centres the traffic lights in it beside the row's buttons; the row ends 4 pt | |
| 36 | short of it, so the tabs stand as near the buttons as a tab bar would. A | |
| 37 | press in the row's empty space drags the window and a double press zooms or | |
| 38 | minimizes it, as Desktop & Dock says; presses in a group of buttons do | |
| 39 | neither, so AppKit is told the view never moves the window. The title is still set, for the Window menu, Mission Control and | |
| 37 | 40 | VoiceOver. `--screenshot` paints the lights where the hidden window's AppKit |
| 38 | 41 | put them. The app draws the row as part of the same frame as the rest of the |
| 39 | 42 | chrome. Under the whole |
arc/ui.md+2-1| ... | ... | @@ -81,7 +81,8 @@ frame N |
| 81 | 81 | pixels, by its label, as a fraction of an ancestor, or by its children. When |
| 82 | 82 | siblings overflow, space sized from an ancestor gives way first. Then a row |
| 83 | 83 | *folds* its groups, boxes with a full and a folded form (`Spec::fold`), by |
| 84 | priority. Only then do boxes give up room by their *strictness*, the least | |
| 84 | priority, reaching into its boxes sized by their children, so a group can | |
| 85 | fold inside another. Only then do boxes give up room by their *strictness*, the least | |
| 85 | 86 | strict first. That one knob covers most of what flexbox is usually needed |
| 86 | 87 | for. A box filling across a parent sized by its children stretches to what |
| 87 | 88 | its siblings make it, and a popup that isn't strict gives way to the window, |
crates/snowbound/assets/icons/styles.svg+2-6| ... | ... | @@ -1,8 +1,4 @@ |
| 1 | 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#79c2ff"/><stop offset="1" stop-color="#1f7cf0"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M2.8 2.35H13.2A0.8 0.8 0 0 1 14 3.15V5.15A0.8 0.8 0 0 1 13.2 5.95H2.8A0.8 0.8 0 0 1 2 5.15V3.15A0.8 0.8 0 0 1 2.8 2.35Z" fill="#000000" fill-opacity="0.16"/> | |
| 6 | <path class="accent" d="M2.8 1.6H13.2A0.8 0.8 0 0 1 14 2.4V4.4A0.8 0.8 0 0 1 13.2 5.2H2.8A0.8 0.8 0 0 1 2 4.4V2.4A0.8 0.8 0 0 1 2.8 1.6Z" fill="url(#g0)" stroke="#1560c4" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/> | |
| 7 | <path d="M2 7.5H11V9H2ZM2 10.5H14V11.5H2ZM2 13H12V14H2Z" fill="currentColor"/> | |
| 2 | <path d="M1.4 13.4L4.8 2.6L8.2 13.4M2.6 9.7H7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path class="accent" d="M14 11.15A2.1 2.1 0 1 1 9.8 11.15A2.1 2.1 0 1 1 14 11.15ZM14 8.7V13.3" fill="none" stroke="#1f6fe0" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/> | |
| 8 | 4 | </svg> |
crates/snowbound/src/aqua.rs+3-2| ... | ... | @@ -77,7 +77,7 @@ pub fn cover_border_line(ui: &mut ui::Ui, width: f32) { |
| 77 | 77 | ui::Spec { |
| 78 | 78 | flags: ui::Flags::FLOAT, |
| 79 | 79 | size: [ui::px(width), ui::px(1.0)], |
| 80 | position: [0.0, crate::TITLE + crate::TAB_ROW - 1.0], | |
| 80 | position: [0.0, crate::TOOLBAR + crate::TAB_ROW - 1.0], | |
| 81 | 81 | fill: Some(draw::srgb(grey, grey, grey)), |
| 82 | 82 | ..ui::Spec::default() |
| 83 | 83 | }, |
| ... | ... | @@ -85,7 +85,8 @@ pub fn cover_border_line(ui: &mut ui::Ui, width: f32) { |
| 85 | 85 | } |
| 86 | 86 | |
| 87 | 87 | /// `-mouseDownCanMoveWindow` for the content view: a textured window drags from any press |
| 88 | /// on a view that allows it, asking once. The app drags from its toolbar's empty space | |
| 88 | /// on a view that allows it, and a title bar the view lies under drags and zooms from it, | |
| 89 | /// buttons drawn there included. The app drags and zooms from its toolbar's empty space | |
| 89 | 90 | /// itself, through `-performWindowDragWithEvent:`. |
| 90 | 91 | pub extern "C" fn no_window_drags(_: &AnyObject, _: Sel) -> Bool { |
| 91 | 92 | Bool::NO |
crates/snowbound/src/macos.rs+21-11| ... | ... | @@ -459,7 +459,7 @@ pub fn install_backdrop(window: &Window) -> bool { |
| 459 | 459 | // Before 10.10 AppKit has no materials; 10.6's textured window stands in, its gradient |
| 460 | 460 | // running through the toolbar and the tab row to the notebook's frame. |
| 461 | 461 | let Some(effect) = AnyClass::get("NSVisualEffectView") else { |
| 462 | return crate::aqua::textured(window, crate::TITLE + crate::TAB_ROW); | |
| 462 | return crate::aqua::textured(window, crate::TOOLBAR + crate::TAB_ROW); | |
| 463 | 463 | }; |
| 464 | 464 | let view = &*ns_view(window); |
| 465 | 465 | unsafe { |
| ... | ... | @@ -1068,15 +1068,27 @@ pub fn set_min_size(window: &Window, size: [f32; 2]) { |
| 1068 | 1068 | window.set_min_inner_size(Some(winit::dpi::LogicalSize::new(size[0], size[1]))); |
| 1069 | 1069 | } |
| 1070 | 1070 | |
| 1071 | /// Zooms the window once the current event is handled: AppKit's zoom animation runs its | |
| 1072 | /// own loop, and started from inside winit's handler it would hold every resize until the | |
| 1073 | /// end, stretching the last frame instead of drawing each step. | |
| 1071 | /// Zooms or minimizes the window, as a double click on a title bar does, once the current | |
| 1072 | /// event is handled: AppKit's zoom animation runs its own loop, and started from inside | |
| 1073 | /// winit's handler it would hold every resize until the end, stretching the last frame | |
| 1074 | /// instead of drawing each step. | |
| 1074 | 1075 | pub fn zoom(window: &Window) { |
| 1075 | 1076 | let window = ns_window(window); |
| 1077 | // What a double click on a title bar does, as Desktop & Dock's setting says. | |
| 1078 | let action: Option<Retained<NSString>> = unsafe { | |
| 1079 | let defaults: Retained<AnyObject> = | |
| 1080 | msg_send_id![class!(NSUserDefaults), standardUserDefaults]; | |
| 1081 | msg_send_id![&defaults, stringForKey: &*NSString::from_str("AppleActionOnDoubleClick")] | |
| 1082 | }; | |
| 1083 | let selector = match action.map(|action| action.to_string()).as_deref() { | |
| 1084 | Some("None") => return, | |
| 1085 | Some("Minimize") => sel!(miniaturize:), | |
| 1086 | _ => sel!(zoom:), | |
| 1087 | }; | |
| 1076 | 1088 | unsafe { |
| 1077 | 1089 | let _: () = msg_send![ |
| 1078 | 1090 | &window, |
| 1079 | performSelector: sel!(zoom:), | |
| 1091 | performSelector: selector, | |
| 1080 | 1092 | withObject: std::ptr::null::<AnyObject>(), |
| 1081 | 1093 | afterDelay: 0.0f64 |
| 1082 | 1094 | ]; |
| ... | ... | @@ -1096,12 +1108,10 @@ pub fn install_text_input(window: &Window) { |
| 1096 | 1108 | sel!(insertText:replacementRange:), |
| 1097 | 1109 | insert_text as unsafe extern "C" fn(_, _, _, _), |
| 1098 | 1110 | ); |
| 1099 | if crate::aqua::before_lion() { | |
| 1100 | class.add_method( | |
| 1101 | sel!(mouseDownCanMoveWindow), | |
| 1102 | crate::aqua::no_window_drags as extern "C" fn(_, _) -> _, | |
| 1103 | ); | |
| 1104 | } | |
| 1111 | class.add_method( | |
| 1112 | sel!(mouseDownCanMoveWindow), | |
| 1113 | crate::aqua::no_window_drags as extern "C" fn(_, _) -> _, | |
| 1114 | ); | |
| 1105 | 1115 | class.register() |
| 1106 | 1116 | }); |
| 1107 | 1117 | assert_eq!(class.superclass(), Some(view.class())); |
crates/snowbound/src/main.rs+293-210| ... | ... | @@ -134,9 +134,13 @@ use winit::{ |
| 134 | 134 | window::CursorIcon, |
| 135 | 135 | }; |
| 136 | 136 | |
| 137 | /// Height of the toolbar's row, which is the title bar where the platform lets it: a | |
| 138 | /// unified compact toolbar's on macOS. | |
| 137 | /// Height of the title bar the toolbar's buttons centre in: a unified compact toolbar's on | |
| 138 | /// macOS, whose traffic lights they line up with. | |
| 139 | 139 | const TITLE: f32 = 38.0; |
| 140 | /// Height of the toolbar's row, which is the title bar where the platform lets it. Its foot | |
| 141 | /// is short of the title bar's, so the tabs below stand as near its buttons as a tab bar | |
| 142 | /// under a compact toolbar does. | |
| 143 | const TOOLBAR: f32 = TITLE - 4.0; | |
| 140 | 144 | /// Space between toolbar groups. |
| 141 | 145 | const GAP: f32 = 6.0; |
| 142 | 146 | /// A tool face's room beside its buttons. |
| ... | ... | @@ -898,6 +902,8 @@ struct State { |
| 898 | 902 | strip_press: Option<Instant>, |
| 899 | 903 | /// The strip is held and the window moves once the pointer does. |
| 900 | 904 | strip_held: bool, |
| 905 | /// The zoom typed in the zoom's field while it is open. | |
| 906 | zoom_typed: Option<String>, | |
| 901 | 907 | /// Where a replay asked the next frame to be written. |
| 902 | 908 | snapshot: Option<PathBuf>, |
| 903 | 909 | /// A replay waiting for nothing to be on its way, and where it wants the accessibility |
| ... | ... | @@ -1247,6 +1253,7 @@ impl State { |
| 1247 | 1253 | pointer: [0.0; 2], |
| 1248 | 1254 | strip_press: None, |
| 1249 | 1255 | strip_held: false, |
| 1256 | zoom_typed: None, | |
| 1250 | 1257 | snapshot: None, |
| 1251 | 1258 | replay_settle: None, |
| 1252 | 1259 | frames: std::env::var_os("SNOWBOUND_FRAMES").map(|dir| (dir.into(), Instant::now())), |
| ... | ... | @@ -1514,7 +1521,7 @@ impl State { |
| 1514 | 1521 | if !self.navigation_bar_right { |
| 1515 | 1522 | self.sidebar(&theme, sidebar); |
| 1516 | 1523 | } |
| 1517 | // Where the notebook button floats over the tab row while the sidebar is shut. | |
| 1524 | // Where the buttons float over the tab row while the sidebar is shut. | |
| 1518 | 1525 | let beside = 1.0 - sidebar / sidebar::WIDTH; |
| 1519 | 1526 | self.ui.open( |
| 1520 | 1527 | "main", |
| ... | ... | @@ -1544,14 +1551,19 @@ impl State { |
| 1544 | 1551 | ); |
| 1545 | 1552 | if !self.temporary { |
| 1546 | 1553 | // Past the row's padding, the frame's corner and the first tab's shadow, the tabs' |
| 1547 | // outlines start where the button's square ends; the room shrinks on the sidebar's | |
| 1548 | // easing, so the tabs ease with it. | |
| 1549 | let room = TAB_ROW - FRAME - self.rounding() - ui::SHADOW[0]; | |
| 1550 | let room = if self.navigation_bar_right { 0.0 } else { room }; | |
| 1554 | // outlines start where Back and Forward end, and on the left the notebook button's | |
| 1555 | // square after them; that room shrinks on the sidebar's easing, as they move into | |
| 1556 | // its header, so the tabs ease with it. | |
| 1557 | let room = sidebar::NAV + TAB_ROW - FRAME - self.rounding() - ui::SHADOW[0]; | |
| 1558 | let room = if self.navigation_bar_right { | |
| 1559 | room - TAB_ROW | |
| 1560 | } else { | |
| 1561 | room * beside | |
| 1562 | }; | |
| 1551 | 1563 | self.ui.leaf( |
| 1552 | 1564 | "rail", |
| 1553 | 1565 | Spec { |
| 1554 | size: [px(room * beside), px(1.0)], | |
| 1566 | size: [px(room), px(1.0)], | |
| 1555 | 1567 | ..Spec::default() |
| 1556 | 1568 | }, |
| 1557 | 1569 | ); |
| ... | ... | @@ -1675,6 +1687,7 @@ impl State { |
| 1675 | 1687 | #[cfg(feature = "live")] |
| 1676 | 1688 | self.avatars(); |
| 1677 | 1689 | if self.session.is_some() { |
| 1690 | self.zoom(&theme)?; | |
| 1678 | 1691 | self.page_tools(&theme); |
| 1679 | 1692 | } |
| 1680 | 1693 | let button = if self.navigation_bar_right && !self.temporary { |
| ... | ... | @@ -1737,7 +1750,7 @@ impl State { |
| 1737 | 1750 | if self.navigation_bar_right { |
| 1738 | 1751 | self.sidebar(&theme, sidebar); |
| 1739 | 1752 | } |
| 1740 | self.sidebar_button(height); | |
| 1753 | self.sidebar_button(height, sidebar); | |
| 1741 | 1754 | self.ui.close(); |
| 1742 | 1755 | self.context_menu(); |
| 1743 | 1756 | return Ok((section, open_tab, None)); |
| ... | ... | @@ -1750,7 +1763,7 @@ impl State { |
| 1750 | 1763 | if self.navigation_bar_right { |
| 1751 | 1764 | self.sidebar(&theme, sidebar); |
| 1752 | 1765 | } |
| 1753 | self.sidebar_button(height); | |
| 1766 | self.sidebar_button(height, sidebar); | |
| 1754 | 1767 | self.ui.close(); |
| 1755 | 1768 | self.context_menu(); |
| 1756 | 1769 | return Ok((section, open_tab, None)); |
| ... | ... | @@ -1836,7 +1849,7 @@ impl State { |
| 1836 | 1849 | self.sidebar(&theme, sidebar); |
| 1837 | 1850 | } |
| 1838 | 1851 | self.task_pane(&theme); |
| 1839 | self.sidebar_button(height); | |
| 1852 | self.sidebar_button(height, sidebar); | |
| 1840 | 1853 | self.ui.close(); |
| 1841 | 1854 | self.context_menu(); |
| 1842 | 1855 | // No tab stands on the frame in Full Page View. |
| ... | ... | @@ -2025,7 +2038,7 @@ impl State { |
| 2025 | 2038 | } else { |
| 2026 | 2039 | Flags::default() |
| 2027 | 2040 | }, |
| 2028 | size: [fill(), px(TITLE)], | |
| 2041 | size: [fill(), px(TOOLBAR)], | |
| 2029 | 2042 | pad: [0.0, (TITLE - ui::shell::TOOL) / 2.0], |
| 2030 | 2043 | gap: GAP, |
| 2031 | 2044 | role: Some(accesskit::Role::Toolbar), |
| ... | ... | @@ -2042,7 +2055,7 @@ impl State { |
| 2042 | 2055 | self.ui.open( |
| 2043 | 2056 | "bar", |
| 2044 | 2057 | Spec { |
| 2045 | size: [fill(), px(TITLE)], | |
| 2058 | size: [fill(), px(TOOLBAR)], | |
| 2046 | 2059 | fill: Some(theme.strip), |
| 2047 | 2060 | pad: [0.0, edge], |
| 2048 | 2061 | offset: [0.0, -edge], |
| ... | ... | @@ -2070,7 +2083,7 @@ impl State { |
| 2070 | 2083 | "tools", |
| 2071 | 2084 | Spec { |
| 2072 | 2085 | flags: Flags::CLIP, |
| 2073 | size: [fill(), px(TITLE)], | |
| 2086 | size: [fill(), px(TOOLBAR)], | |
| 2074 | 2087 | pad: [face.pad[0], edge], |
| 2075 | 2088 | offset: [0.0, -edge], |
| 2076 | 2089 | gap: if theme.tool[3] > 0.0 && !panel { |
| ... | ... | @@ -2121,7 +2134,6 @@ impl State { |
| 2121 | 2134 | use Entry::{Open, Rule, Run}; |
| 2122 | 2135 | use canvas::editor::{Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, Toggle}; |
| 2123 | 2136 | use commands::{Choice, Id as Cmd}; |
| 2124 | use ui::shell::TOOL; | |
| 2125 | 2137 | // A focused picture has no text to show a format for. |
| 2126 | 2138 | let state = self.format_state(); |
| 2127 | 2139 | let statuses: Vec<_> = commands::COMMANDS |
| ... | ... | @@ -2145,90 +2157,73 @@ impl State { |
| 2145 | 2157 | let engine = &self.view.engine; |
| 2146 | 2158 | let session = self.session.as_ref(); |
| 2147 | 2159 | let update = self.updates.status(); |
| 2148 | let label = format!("{:.0}%", self.view.zoom() * 100.0); | |
| 2149 | 2160 | let sheet = self.gallery_sheet(); |
| 2150 | 2161 | let drawing_pens = self.pens(); |
| 2151 | 2162 | let ui = &mut self.ui; |
| 2152 | 2163 | let text = theme.text; |
| 2153 | 2164 | let tags: Vec<_> = self.tags.iter().enumerate().collect(); |
| 2165 | let face = tool_face(theme, grouped(theme), FACE_RISE); | |
| 2154 | 2166 | let mut choice = group( |
| 2155 | 2167 | ui, |
| 2156 | 2168 | "navigate", |
| 2157 | 2169 | 6, |
| 2170 | face.clone(), | |
| 2158 | 2171 | |ui| { |
| 2159 | 2172 | let mut choice = None; |
| 2160 | for id in [Cmd::Back, Cmd::Forward, Cmd::Undo, Cmd::Redo] { | |
| 2173 | for id in [Cmd::Undo, Cmd::Redo] { | |
| 2161 | 2174 | choice = tool(ui, id, status_of(id)).or(choice); |
| 2162 | 2175 | } |
| 2163 | 2176 | choice |
| 2164 | 2177 | }, |
| 2165 | 2178 | |ui| { |
| 2166 | let entries = [Run(Cmd::Redo), Rule, Run(Cmd::Back), Run(Cmd::Forward)]; | |
| 2167 | dropdown(ui, "menu", Head::Split(Cmd::Undo), &entries, status_of) | |
| 2168 | }, | |
| 2169 | ); | |
| 2170 | choice = group( | |
| 2171 | ui, | |
| 2172 | "clipboard", | |
| 2173 | 5, | |
| 2174 | |ui| { | |
| 2175 | divider(ui, theme); | |
| 2176 | let mut choice = None; | |
| 2177 | let paste = ui.id("paste"); | |
| 2178 | if !status_of(Cmd::Paste).enabled { | |
| 2179 | ui::shell::unavailable(ui, "paste", art::PASTE, text, true); | |
| 2180 | } else if ui::shell::split_button( | |
| 2179 | dropdown( | |
| 2181 | 2180 | ui, |
| 2182 | "paste", | |
| 2183 | title(Cmd::Paste), | |
| 2184 | art::PASTE, | |
| 2185 | None, | |
| 2186 | None, | |
| 2187 | toolbar_popup("paste"), | |
| 2181 | "menu", | |
| 2182 | Head::Split(Cmd::Undo), | |
| 2183 | &[Run(Cmd::Redo)], | |
| 2184 | status_of, | |
| 2188 | 2185 | ) |
| 2189 | .clicked | |
| 2190 | { | |
| 2191 | choice = Some(Choice::Command(Cmd::Paste)); | |
| 2192 | } | |
| 2193 | tip(ui, Cmd::Paste); | |
| 2194 | // Pasting keeps only the text until #25. | |
| 2195 | let options = [ | |
| 2196 | ("Keep Source Formatting", false), | |
| 2197 | ("Merge Formatting", false), | |
| 2198 | ("Keep Text Only", true), | |
| 2199 | ("Picture", false), | |
| 2200 | ]; | |
| 2201 | let mut items = vec![ui::popup::Item { | |
| 2202 | text: "Paste Options", | |
| 2203 | heading: true, | |
| 2204 | ..Default::default() | |
| 2205 | }]; | |
| 2206 | items.extend(options.map(|(text, enabled)| ui::popup::Item { | |
| 2207 | text, | |
| 2208 | disabled: !enabled, | |
| 2209 | ..Default::default() | |
| 2210 | })); | |
| 2211 | let anchor = ui::Anchor::Below(ui.rect(paste).unwrap_or_default()); | |
| 2212 | if ui::popup::menu(ui, toolbar_popup("paste"), anchor, &items, None).is_some() { | |
| 2213 | choice = Some(Choice::Command(Cmd::Paste)); | |
| 2214 | } | |
| 2215 | for id in [Cmd::Cut, Cmd::Copy, Cmd::FormatPainter] { | |
| 2216 | choice = tool(ui, id, status_of(id)).or(choice); | |
| 2217 | } | |
| 2218 | choice | |
| 2219 | 2186 | }, |
| 2220 | |ui| { | |
| 2221 | let entries = [ | |
| 2222 | Run(Cmd::Cut), | |
| 2223 | Run(Cmd::Copy), | |
| 2224 | Run(Cmd::FormatPainter), | |
| 2225 | Rule, | |
| 2226 | Open("paste", Cmd::Paste), | |
| 2227 | ]; | |
| 2228 | dropdown(ui, "menu", Head::Split(Cmd::Paste), &entries, status_of) | |
| 2187 | ); | |
| 2188 | ui.open( | |
| 2189 | "clipboard", | |
| 2190 | Spec { | |
| 2191 | flags: Flags::CLICKABLE, | |
| 2192 | ..face.clone() | |
| 2229 | 2193 | }, |
| 2230 | ) | |
| 2231 | .or(choice); | |
| 2194 | ); | |
| 2195 | divider(ui, theme); | |
| 2196 | let entries = [ | |
| 2197 | Run(Cmd::Paste), | |
| 2198 | Open("paste", Cmd::Paste), | |
| 2199 | Rule, | |
| 2200 | Run(Cmd::Cut), | |
| 2201 | Run(Cmd::Copy), | |
| 2202 | ]; | |
| 2203 | let head = Head::Menu("Clipboard", art::PASTE); | |
| 2204 | choice = dropdown(ui, "menu", head, &entries, status_of).or(choice); | |
| 2205 | // Pasting keeps only the text until #25. | |
| 2206 | let options = [ | |
| 2207 | ("Keep Source Formatting", false), | |
| 2208 | ("Merge Formatting", false), | |
| 2209 | ("Keep Text Only", true), | |
| 2210 | ("Picture", false), | |
| 2211 | ]; | |
| 2212 | let mut items = vec![ui::popup::Item { | |
| 2213 | text: "Paste Options", | |
| 2214 | heading: true, | |
| 2215 | ..Default::default() | |
| 2216 | }]; | |
| 2217 | items.extend(options.map(|(text, enabled)| ui::popup::Item { | |
| 2218 | text, | |
| 2219 | disabled: !enabled, | |
| 2220 | ..Default::default() | |
| 2221 | })); | |
| 2222 | let anchor = ui::Anchor::Below(ui.rect(ui.id("menu")).unwrap_or_default()); | |
| 2223 | if ui::popup::menu(ui, toolbar_popup("paste"), anchor, &items, None).is_some() { | |
| 2224 | choice = Some(Choice::Command(Cmd::Paste)); | |
| 2225 | } | |
| 2226 | ui.close(); | |
| 2232 | 2227 | let font = state.font.clone().unwrap_or_default(); |
| 2233 | 2228 | let size = state |
| 2234 | 2229 | .font_size |
| ... | ... | @@ -2236,12 +2231,13 @@ impl State { |
| 2236 | 2231 | ui.open( |
| 2237 | 2232 | "font", |
| 2238 | 2233 | Spec { |
| 2234 | flags: Flags::CLICKABLE, | |
| 2239 | 2235 | gap: 1.0, |
| 2240 | ..tool_face(theme, grouped(theme), FACE_RISE) | |
| 2236 | ..face.clone() | |
| 2241 | 2237 | }, |
| 2242 | 2238 | ); |
| 2243 | 2239 | divider(ui, theme); |
| 2244 | // Styles sits left of the font box (Clover, 2026-09-30), and like it never folds. | |
| 2240 | // Styles, the font and the size never fold. | |
| 2245 | 2241 | let styles = toolbar_popup("styles"); |
| 2246 | 2242 | let anchor = if status_of(Cmd::Styles).enabled { |
| 2247 | 2243 | ui::shell::menu_button(ui, "styles", art::STYLES, None, styles) |
| ... | ... | @@ -2342,14 +2338,22 @@ impl State { |
| 2342 | 2338 | let id = Cmd::Toggle(toggle); |
| 2343 | 2339 | choice = tool(ui, id, status_of(id)).or(choice); |
| 2344 | 2340 | } |
| 2345 | ui.close(); | |
| 2341 | let effects = [ | |
| 2342 | Run(Cmd::Toggle(Toggle::Strikethrough)), | |
| 2343 | Run(Cmd::Toggle(Toggle::Subscript)), | |
| 2344 | Run(Cmd::Toggle(Toggle::Superscript)), | |
| 2345 | Rule, | |
| 2346 | Run(Cmd::ClearFormatting), | |
| 2347 | ]; | |
| 2348 | // The rest of the group folds inside it, on its face. | |
| 2346 | 2349 | choice = group( |
| 2347 | 2350 | ui, |
| 2348 | 2351 | "character", |
| 2349 | 2352 | 8, |
| 2353 | Spec::default(), | |
| 2350 | 2354 | |ui| { |
| 2351 | divider(ui, theme); | |
| 2352 | let mut choice = None; | |
| 2355 | let head = Head::Menu("Text Effects", art::STRIKETHROUGH); | |
| 2356 | let mut choice = dropdown(ui, "script", head, &effects, status_of); | |
| 2353 | 2357 | // Each button applies its menu's last pick: the highlighter shows it in its |
| 2354 | 2358 | // artwork, the font colour in a bar. |
| 2355 | 2359 | for (part, id, swatches, columns, none, bar) in [ |
| ... | ... | @@ -2410,37 +2414,23 @@ impl State { |
| 2410 | 2414 | }); |
| 2411 | 2415 | } |
| 2412 | 2416 | } |
| 2413 | dropdown( | |
| 2414 | ui, | |
| 2415 | "script", | |
| 2416 | Head::Menu("Text Effects", art::STRIKETHROUGH), | |
| 2417 | &[ | |
| 2418 | Run(Cmd::Toggle(Toggle::Strikethrough)), | |
| 2419 | Run(Cmd::Toggle(Toggle::Subscript)), | |
| 2420 | Run(Cmd::Toggle(Toggle::Superscript)), | |
| 2421 | Rule, | |
| 2422 | Run(Cmd::ClearFormatting), | |
| 2423 | ], | |
| 2424 | status_of, | |
| 2425 | ) | |
| 2426 | .or(choice) | |
| 2417 | tool(ui, Cmd::FormatPainter, status_of(Cmd::FormatPainter)).or(choice) | |
| 2427 | 2418 | }, |
| 2428 | 2419 | |ui| { |
| 2429 | let entries = [ | |
| 2420 | let mut entries = effects.to_vec(); | |
| 2421 | entries.extend([ | |
| 2422 | Rule, | |
| 2430 | 2423 | Open("highlight", Cmd::Highlight), |
| 2431 | 2424 | Run(Cmd::FontColor), |
| 2432 | 2425 | Open("color", Cmd::FontColor), |
| 2433 | 2426 | Rule, |
| 2434 | Run(Cmd::Toggle(Toggle::Strikethrough)), | |
| 2435 | Run(Cmd::Toggle(Toggle::Subscript)), | |
| 2436 | Run(Cmd::Toggle(Toggle::Superscript)), | |
| 2437 | Rule, | |
| 2438 | Run(Cmd::ClearFormatting), | |
| 2439 | ]; | |
| 2427 | Run(Cmd::FormatPainter), | |
| 2428 | ]); | |
| 2440 | 2429 | dropdown(ui, "menu", Head::Split(Cmd::Highlight), &entries, status_of) |
| 2441 | 2430 | }, |
| 2442 | 2431 | ) |
| 2443 | 2432 | .or(choice); |
| 2433 | ui.close(); | |
| 2444 | 2434 | let alignments = [Alignment::Left, Alignment::Center, Alignment::Right].map(Cmd::Align); |
| 2445 | 2435 | let paragraph = [ |
| 2446 | 2436 | Rule, |
| ... | ... | @@ -2455,6 +2445,7 @@ impl State { |
| 2455 | 2445 | ui, |
| 2456 | 2446 | "paragraph", |
| 2457 | 2447 | 7, |
| 2448 | face.clone(), | |
| 2458 | 2449 | |ui| { |
| 2459 | 2450 | divider(ui, theme); |
| 2460 | 2451 | let mut choice = None; |
| ... | ... | @@ -2603,6 +2594,7 @@ impl State { |
| 2603 | 2594 | ui, |
| 2604 | 2595 | "tags", |
| 2605 | 2596 | 9, |
| 2597 | face.clone(), | |
| 2606 | 2598 | |ui| { |
| 2607 | 2599 | divider(ui, theme); |
| 2608 | 2600 | let mut choice = None; |
| ... | ... | @@ -2659,6 +2651,7 @@ impl State { |
| 2659 | 2651 | ui, |
| 2660 | 2652 | "insert", |
| 2661 | 2653 | 4, |
| 2654 | face.clone(), | |
| 2662 | 2655 | |ui| { |
| 2663 | 2656 | divider(ui, theme); |
| 2664 | 2657 | let mut choice = None; |
| ... | ... | @@ -2742,6 +2735,7 @@ impl State { |
| 2742 | 2735 | ui, |
| 2743 | 2736 | part, |
| 2744 | 2737 | priority, |
| 2738 | face.clone(), | |
| 2745 | 2739 | |ui| { |
| 2746 | 2740 | divider(ui, theme); |
| 2747 | 2741 | let mut choice = None; |
| ... | ... | @@ -2760,6 +2754,7 @@ impl State { |
| 2760 | 2754 | ui, |
| 2761 | 2755 | "draw", |
| 2762 | 2756 | 2, |
| 2757 | face.clone(), | |
| 2763 | 2758 | |ui| { |
| 2764 | 2759 | divider(ui, theme); |
| 2765 | 2760 | let mut choice = tool(ui, Cmd::SelectType, status_of(Cmd::SelectType)); |
| ... | ... | @@ -2805,6 +2800,40 @@ impl State { |
| 2805 | 2800 | |_| None, |
| 2806 | 2801 | ) |
| 2807 | 2802 | .or(choice); |
| 2803 | // Folded, Page Color stays in the view's menu at the row's end. | |
| 2804 | choice = group( | |
| 2805 | ui, | |
| 2806 | "page color", | |
| 2807 | 1, | |
| 2808 | face.clone(), | |
| 2809 | |ui| { | |
| 2810 | divider(ui, theme); | |
| 2811 | if !status_of(Cmd::PageColor).enabled { | |
| 2812 | ui::shell::unavailable(ui, "page color", art::PAGE_COLOR, text, true); | |
| 2813 | tip(ui, Cmd::PageColor); | |
| 2814 | return None; | |
| 2815 | } | |
| 2816 | let menu = toolbar_popup("page color"); | |
| 2817 | let anchor = ui::shell::menu_button(ui, "page color", art::PAGE_COLOR, None, menu); | |
| 2818 | tip(ui, Cmd::PageColor); | |
| 2819 | background::menu( | |
| 2820 | ui, | |
| 2821 | menu, | |
| 2822 | anchor, | |
| 2823 | &mut self.thumbnails, | |
| 2824 | &mut self.templates, | |
| 2825 | // The page's own colour aside. | |
| 2826 | canvas::gpu::Paper { | |
| 2827 | color: ui.theme.paper, | |
| 2828 | ink: ui.theme.paper_ink, | |
| 2829 | }, | |
| 2830 | self.view.editor.page_color(), | |
| 2831 | self.view.editor.rule_lines(), | |
| 2832 | ) | |
| 2833 | }, | |
| 2834 | |_| None, | |
| 2835 | ) | |
| 2836 | .or(choice); | |
| 2808 | 2837 | ui.leaf( |
| 2809 | 2838 | "space", |
| 2810 | 2839 | Spec { |
| ... | ... | @@ -2815,7 +2844,14 @@ impl State { |
| 2815 | 2844 | if let Some(session) = session { |
| 2816 | 2845 | sync::control(ui, session, &update, theme); |
| 2817 | 2846 | } |
| 2847 | // The view's commands, with zoom and Page Color for where theirs have folded away. | |
| 2818 | 2848 | let views = [ |
| 2849 | Run(Cmd::ZoomIn), | |
| 2850 | Run(Cmd::ZoomOut), | |
| 2851 | Run(Cmd::ActualSize), | |
| 2852 | Rule, | |
| 2853 | Open("page color", Cmd::PageColor), | |
| 2854 | Rule, | |
| 2819 | 2855 | Run(Cmd::Sidebar), |
| 2820 | 2856 | Run(Cmd::PageList), |
| 2821 | 2857 | Run(Cmd::PagesMatchTheme), |
| ... | ... | @@ -2824,95 +2860,151 @@ impl State { |
| 2824 | 2860 | Run(Cmd::HideSpelling), |
| 2825 | 2861 | Run(Cmd::Spelling), |
| 2826 | 2862 | ]; |
| 2827 | choice = group( | |
| 2828 | ui, | |
| 2863 | ui.open( | |
| 2829 | 2864 | "view", |
| 2830 | 1, | |
| 2831 | |ui| { | |
| 2832 | divider(ui, theme); | |
| 2833 | let mut choice = None; | |
| 2834 | if status_of(Cmd::PageColor).enabled { | |
| 2835 | let anchor = ui::shell::menu_button( | |
| 2836 | ui, | |
| 2837 | "page color", | |
| 2838 | art::PAGE_COLOR, | |
| 2839 | None, | |
| 2840 | toolbar_popup("page color"), | |
| 2841 | ); | |
| 2842 | tip(ui, Cmd::PageColor); | |
| 2843 | choice = background::menu( | |
| 2844 | ui, | |
| 2845 | toolbar_popup("page color"), | |
| 2846 | anchor, | |
| 2847 | &mut self.thumbnails, | |
| 2848 | &mut self.templates, | |
| 2849 | // The page's own colour aside. | |
| 2850 | canvas::gpu::Paper { | |
| 2851 | color: ui.theme.paper, | |
| 2852 | ink: ui.theme.paper_ink, | |
| 2853 | }, | |
| 2854 | self.view.editor.page_color(), | |
| 2855 | self.view.editor.rule_lines(), | |
| 2856 | ) | |
| 2857 | .or(choice.take()); | |
| 2858 | } else { | |
| 2859 | ui::shell::unavailable(ui, "page color", art::PAGE_COLOR, text, true); | |
| 2860 | tip(ui, Cmd::PageColor); | |
| 2861 | } | |
| 2862 | choice = tool(ui, Cmd::ZoomOut, status_of(Cmd::ZoomOut)).or(choice); | |
| 2863 | let level = ui.leaf( | |
| 2864 | "level", | |
| 2865 | Spec { | |
| 2866 | flags: Flags::CLICKABLE, | |
| 2867 | size: [px(44.0), px(TOOL)], | |
| 2868 | text: Some(&label), | |
| 2869 | hover_fill: Some(theme.hover()), | |
| 2870 | radius: 4.0, | |
| 2871 | center: true, | |
| 2872 | role: Some(accesskit::Role::Button), | |
| 2873 | ..Spec::default() | |
| 2874 | }, | |
| 2875 | ); | |
| 2876 | if level.clicked { | |
| 2877 | choice = Some(Choice::Command(Cmd::ActualSize)); | |
| 2878 | } | |
| 2879 | tip(ui, Cmd::ActualSize); | |
| 2880 | if let Some(node) = ui.access(ui.id("level")) { | |
| 2881 | node.set_value(label.as_str()); | |
| 2882 | } | |
| 2883 | choice = tool(ui, Cmd::ZoomIn, status_of(Cmd::ZoomIn)).or(choice); | |
| 2884 | dropdown( | |
| 2885 | ui, | |
| 2886 | "view", | |
| 2887 | Head::More("More View Options"), | |
| 2888 | &views, | |
| 2889 | status_of, | |
| 2890 | ) | |
| 2891 | .or(choice) | |
| 2865 | Spec { | |
| 2866 | flags: Flags::CLICKABLE, | |
| 2867 | ..face.clone() | |
| 2892 | 2868 | }, |
| 2893 | |ui| { | |
| 2894 | let mut entries = vec![ | |
| 2895 | Run(Cmd::ZoomIn), | |
| 2896 | Run(Cmd::ZoomOut), | |
| 2897 | Run(Cmd::ActualSize), | |
| 2898 | Rule, | |
| 2899 | Open("page color", Cmd::PageColor), | |
| 2900 | Rule, | |
| 2901 | ]; | |
| 2902 | entries.extend(views); | |
| 2903 | dropdown( | |
| 2904 | ui, | |
| 2905 | "menu", | |
| 2906 | Head::Menu("View", art::ZOOM_IN), | |
| 2907 | &entries, | |
| 2908 | status_of, | |
| 2909 | ) | |
| 2869 | ); | |
| 2870 | let head = Head::More("More View Options"); | |
| 2871 | choice = dropdown(ui, "more", head, &views, status_of).or(choice); | |
| 2872 | ui.close(); | |
| 2873 | if let Some(choice) = choice { | |
| 2874 | self.choose(choice); | |
| 2875 | } | |
| 2876 | } | |
| 2877 | ||
| 2878 | /// The page's zoom, left of the search box: Zoom Out, the level and Zoom In joined as one | |
| 2879 | /// control, which folds away first where the tab row lacks room. Clicking the level opens | |
| 2880 | /// it as a field, where Enter zooms to the percentage typed and Escape keeps the zoom. | |
| 2881 | fn zoom(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> { | |
| 2882 | use commands::{Choice, Id as Cmd}; | |
| 2883 | use ui::shell::TOOL; | |
| 2884 | const LEVEL: f32 = 44.0; | |
| 2885 | let format = self.format_state(); | |
| 2886 | let [out, into] = | |
| 2887 | [Cmd::ZoomOut, Cmd::ZoomIn].map(|id| self.status(&Choice::Command(id), &format)); | |
| 2888 | let level = format!("{:.0}%", self.view.zoom() * 100.0); | |
| 2889 | let field = self.ui.id("zoom field"); | |
| 2890 | if self.ui.focused() != Some(field) { | |
| 2891 | self.zoom_typed = None; | |
| 2892 | } | |
| 2893 | self.ui.open( | |
| 2894 | "zoom", | |
| 2895 | Spec { | |
| 2896 | flags: Flags::CLICKABLE, | |
| 2897 | fold: Some(0), | |
| 2898 | ..Spec::default() | |
| 2910 | 2899 | }, |
| 2911 | ) | |
| 2912 | .or(choice); | |
| 2900 | ); | |
| 2901 | self.ui.open( | |
| 2902 | "full", | |
| 2903 | Spec { | |
| 2904 | size: [children(), px(TAB_ROW)], | |
| 2905 | // As far from the search box as the page list's buttons are apart. | |
| 2906 | pad: [3.0, (TAB_ROW - TOOL) / 2.0], | |
| 2907 | ..Spec::default() | |
| 2908 | }, | |
| 2909 | ); | |
| 2910 | let typing = self.zoom_typed.is_some(); | |
| 2911 | self.ui.open( | |
| 2912 | "control", | |
| 2913 | Spec { | |
| 2914 | size: [children(), px(TOOL)], | |
| 2915 | fill: Some(theme.base), | |
| 2916 | border: Some(if typing { theme.accent } else { theme.chip }), | |
| 2917 | radius: 4.0, | |
| 2918 | ..Spec::default() | |
| 2919 | }, | |
| 2920 | ); | |
| 2921 | let line = |ui: &mut Ui, part| { | |
| 2922 | ui.leaf( | |
| 2923 | part, | |
| 2924 | Spec { | |
| 2925 | size: [px(1.0), px(TOOL)], | |
| 2926 | fill: Some(theme.chip), | |
| 2927 | ..Spec::default() | |
| 2928 | }, | |
| 2929 | ); | |
| 2930 | }; | |
| 2931 | let mut choice = tool(&mut self.ui, Cmd::ZoomOut, out); | |
| 2932 | line(&mut self.ui, "before"); | |
| 2933 | let mut zoom = None; | |
| 2934 | if let Some(typed) = &mut self.zoom_typed { | |
| 2935 | let signal = ui::text_field( | |
| 2936 | &mut self.ui, | |
| 2937 | field, | |
| 2938 | typed, | |
| 2939 | "", | |
| 2940 | Spec { | |
| 2941 | size: [px(LEVEL), px(TOOL)], | |
| 2942 | pad: [4.0, 0.0], | |
| 2943 | ..Spec::default() | |
| 2944 | }, | |
| 2945 | ); | |
| 2946 | name(&mut self.ui, field, "Zoom"); | |
| 2947 | let key = signal.events.iter().find_map(|event| match event { | |
| 2948 | ui::Event::Key { | |
| 2949 | key: Key::Named(key @ (NamedKey::Enter | NamedKey::Escape)), | |
| 2950 | .. | |
| 2951 | } => Some(*key), | |
| 2952 | _ => None, | |
| 2953 | }); | |
| 2954 | if let Some(key) = key { | |
| 2955 | let percent = typed.trim().trim_end_matches('%').trim().parse::<f32>(); | |
| 2956 | zoom = percent.ok().filter(|_| key == NamedKey::Enter); | |
| 2957 | self.zoom_typed = None; | |
| 2958 | self.ui.set_focus(Some(page())); | |
| 2959 | } | |
| 2960 | } else { | |
| 2961 | let id = self.ui.id("level"); | |
| 2962 | let signal = self.ui.leaf( | |
| 2963 | "level", | |
| 2964 | Spec { | |
| 2965 | // Zoom applies where its buttons do. | |
| 2966 | flags: if out.enabled { | |
| 2967 | Flags::CLICKABLE | |
| 2968 | } else { | |
| 2969 | Flags::default() | |
| 2970 | }, | |
| 2971 | size: [px(LEVEL), px(TOOL)], | |
| 2972 | text: Some(&level), | |
| 2973 | color: Some(if out.enabled { | |
| 2974 | theme.text | |
| 2975 | } else { | |
| 2976 | theme.text_dim | |
| 2977 | }), | |
| 2978 | hover_fill: Some(theme.hover()), | |
| 2979 | center: true, | |
| 2980 | role: Some(accesskit::Role::Button), | |
| 2981 | ..Spec::default() | |
| 2982 | }, | |
| 2983 | ); | |
| 2984 | if let Some(node) = self.ui.access(id) { | |
| 2985 | node.set_label("Zoom"); | |
| 2986 | node.set_value(level.as_str()); | |
| 2987 | } | |
| 2988 | if signal.clicked { | |
| 2989 | self.zoom_typed = Some(level.trim_end_matches('%').to_owned()); | |
| 2990 | self.ui.focus_all(field); | |
| 2991 | } | |
| 2992 | } | |
| 2993 | line(&mut self.ui, "after"); | |
| 2994 | choice = tool(&mut self.ui, Cmd::ZoomIn, into).or(choice); | |
| 2995 | self.ui.close(); | |
| 2996 | self.ui.close(); | |
| 2997 | self.ui.open("folded", Spec::default()); | |
| 2998 | self.ui.close(); | |
| 2999 | self.ui.close(); | |
| 2913 | 3000 | if let Some(choice) = choice { |
| 2914 | 3001 | self.choose(choice); |
| 2915 | 3002 | } |
| 3003 | if let Some(percent) = zoom { | |
| 3004 | let response = self.view.set_zoom(percent / 100.0)?; | |
| 3005 | self.respond(response); | |
| 3006 | } | |
| 3007 | Ok(()) | |
| 2916 | 3008 | } |
| 2917 | 3009 | |
| 2918 | 3010 | /// The search box and the page list's buttons, above the list. Where the tab row lacks |
| ... | ... | @@ -2920,10 +3012,12 @@ impl State { |
| 2920 | 3012 | /// on the page it stays. |
| 2921 | 3013 | fn page_tools(&mut self, theme: &Theme) { |
| 2922 | 3014 | let finding = self.search.finding; |
| 3015 | // It folds after the zoom. | |
| 2923 | 3016 | self.ui.open( |
| 2924 | 3017 | "tools", |
| 2925 | 3018 | Spec { |
| 2926 | fold: (!finding).then_some(0), | |
| 3019 | flags: Flags::CLICKABLE, | |
| 3020 | fold: (!finding).then_some(1), | |
| 2927 | 3021 | ..Spec::default() |
| 2928 | 3022 | }, |
| 2929 | 3023 | ); |
| ... | ... | @@ -5131,19 +5225,21 @@ fn pen_gallery( |
| 5131 | 5225 | ) |
| 5132 | 5226 | } |
| 5133 | 5227 | |
| 5134 | /// A toolbar group that folds by `priority`: `full` builds its full form, `folded` the form | |
| 5135 | /// it folds to. Returns what either chose. | |
| 5228 | /// A toolbar group on `face` that folds by `priority`: `full` builds its full form, `folded` | |
| 5229 | /// the form it folds to. Returns what either chose. Like every group, it takes presses | |
| 5230 | /// between its buttons, so only the row's empty space drags and zooms the window. | |
| 5136 | 5231 | fn group( |
| 5137 | 5232 | ui: &mut Ui, |
| 5138 | 5233 | part: &str, |
| 5139 | 5234 | priority: u32, |
| 5235 | face: Spec<'static>, | |
| 5140 | 5236 | full: impl FnOnce(&mut Ui) -> Option<commands::Choice>, |
| 5141 | 5237 | folded: impl FnOnce(&mut Ui) -> Option<commands::Choice>, |
| 5142 | 5238 | ) -> Option<commands::Choice> { |
| 5143 | let face = tool_face(&ui.theme, grouped(&ui.theme), FACE_RISE); | |
| 5144 | 5239 | ui.open( |
| 5145 | 5240 | part, |
| 5146 | 5241 | Spec { |
| 5242 | flags: Flags::CLICKABLE, | |
| 5147 | 5243 | fold: Some(priority), |
| 5148 | 5244 | ..face |
| 5149 | 5245 | }, |
| ... | ... | @@ -5239,13 +5335,9 @@ fn dropdown( |
| 5239 | 5335 | anchor |
| 5240 | 5336 | } |
| 5241 | 5337 | // A menu of nothing that applies fades as a button would. |
| 5242 | Head::Menu(label, _) | Head::Tools(label, _) | Head::More(label) if !usable => { | |
| 5243 | let (icon, arrow) = match head { | |
| 5244 | Head::Menu(_, icon) | Head::Tools(_, icon) => (icon, true), | |
| 5245 | _ => (ui::shell::CHEVRON, false), | |
| 5246 | }; | |
| 5338 | Head::Menu(label, icon) | Head::Tools(label, icon) if !usable => { | |
| 5247 | 5339 | let tint = ui.theme.text; |
| 5248 | ui::shell::unavailable(ui, part, icon, tint, arrow); | |
| 5340 | ui::shell::unavailable(ui, part, icon, tint, true); | |
| 5249 | 5341 | name(ui, button, label); |
| 5250 | 5342 | ui::Anchor::Below(ui.rect(button).unwrap_or_default()) |
| 5251 | 5343 | } |
| ... | ... | @@ -5264,18 +5356,9 @@ fn dropdown( |
| 5264 | 5356 | anchor |
| 5265 | 5357 | } |
| 5266 | 5358 | Head::More(label) => { |
| 5267 | let open = ui.popup_open(menu); | |
| 5268 | let text = ui.theme.text; | |
| 5269 | if ui::shell::tool_button(ui, part, ui::shell::CHEVRON, text, Some(open)).pressed { | |
| 5270 | ui.open_popup(menu); | |
| 5271 | } | |
| 5272 | if let Some(node) = ui.access(button) { | |
| 5273 | node.set_label(label); | |
| 5274 | node.clear_toggled(); | |
| 5275 | node.set_has_popup(accesskit::HasPopup::Menu); | |
| 5276 | node.set_expanded(open); | |
| 5277 | } | |
| 5278 | ui::Anchor::Below(ui.rect(button).unwrap_or_default()) | |
| 5359 | let anchor = ui::shell::more_button(ui, part, menu, usable); | |
| 5360 | name(ui, button, label); | |
| 5361 | anchor | |
| 5279 | 5362 | } |
| 5280 | 5363 | }; |
| 5281 | 5364 | let keys: Vec<_> = entries |
crates/snowbound/src/screenshot/snow_leopard.rs+1-1| ... | ... | @@ -181,7 +181,7 @@ fn window( |
| 181 | 181 | ) -> Result<Vec<[f32; 4]>, Box<dyn Error>> { |
| 182 | 182 | let [highlight, top, bottom, body] = GREYS[usize::from(!key)]; |
| 183 | 183 | // The gradient runs from under the highlight to the top content border's last row. |
| 184 | let end = TITLE_LINE + (crate::TITLE + crate::TAB_ROW) as usize - 1; | |
| 184 | let end = TITLE_LINE + (crate::TOOLBAR + crate::TAB_ROW) as usize - 1; | |
| 185 | 185 | let total = height + TITLE_LINE; |
| 186 | 186 | let mut pixels: Vec<[f32; 4]> = (0..total) |
| 187 | 187 | .flat_map(|y| { |
crates/snowbound/src/sidebar.rs+86-45| ... | ... | @@ -18,6 +18,13 @@ const RAIL: f32 = crate::TAB_ROW; |
| 18 | 18 | /// on the row's foot, so their labels sit 1 to 3 pixels low. |
| 19 | 19 | const DROP: f32 = 2.0; |
| 20 | 20 | const ROW: f32 = 24.0; |
| 21 | /// The tree's margin, and how far inside a row its icon starts and its label after that; the | |
| 22 | /// header's title and the notebook button's icon, while the sidebar is open, line up with them. | |
| 23 | const MARGIN: f32 = 4.0; | |
| 24 | const ROW_PAD: f32 = 6.0; | |
| 25 | const ICON: f32 = 16.0; | |
| 26 | /// The Back and Forward buttons' box, with room beside them as the notebook button's square has. | |
| 27 | pub const NAV: f32 = 2.0 * ui::shell::TOOL + 1.0 + (RAIL - ui::shell::TOOL); | |
| 21 | 28 | /// How far each level of the tree sits inside its parent. |
| 22 | 29 | const INDENT: f32 = 16.0; |
| 23 | 30 | |
| ... | ... | @@ -136,7 +143,8 @@ pub fn header() -> Id { |
| 136 | 143 | |
| 137 | 144 | /// The sidebar's header row, as tall as the tab row and dragging the window when `drags`, then |
| 138 | 145 | /// with `rows` the tree of `notebooks` with the open section marked. The notebook button |
| 139 | /// floats over the header's end at the window's edge, the `right` one or the left. | |
| 146 | /// floats over the header's icon on the left, or its end on the `right`, where the Back and | |
| 147 | /// Forward buttons otherwise do. | |
| 140 | 148 | fn sidebar( |
| 141 | 149 | ui: &mut Ui, |
| 142 | 150 | tree: &mut Tree, |
| ... | ... | @@ -155,36 +163,36 @@ fn sidebar( |
| 155 | 163 | Flags::default() |
| 156 | 164 | }, |
| 157 | 165 | size: [fill(), px(RAIL)], |
| 158 | pad: [crate::FRAME, (RAIL - ui::shell::TOOL) / 2.0 + DROP], | |
| 159 | gap: 6.0, | |
| 166 | pad: [MARGIN + ROW_PAD, (RAIL - ui::shell::TOOL) / 2.0 + DROP], | |
| 167 | gap: ROW_PAD, | |
| 168 | ..Spec::default() | |
| 169 | }, | |
| 170 | ); | |
| 171 | ui.leaf( | |
| 172 | "icon", | |
| 173 | Spec { | |
| 174 | size: [px(ICON), px(ui::shell::TOOL)], | |
| 160 | 175 | ..Spec::default() |
| 161 | 176 | }, |
| 162 | 177 | ); |
| 163 | // The notebook button's square, which floats over this place. | |
| 164 | let toggle = |ui: &mut Ui| { | |
| 165 | ui.leaf( | |
| 166 | "toggle", | |
| 167 | Spec { | |
| 168 | size: [px(RAIL - crate::FRAME), px(ui::shell::TOOL)], | |
| 169 | ..Spec::default() | |
| 170 | }, | |
| 171 | ); | |
| 172 | }; | |
| 173 | if !right { | |
| 174 | toggle(ui); | |
| 175 | } | |
| 176 | 178 | ui.leaf( |
| 177 | 179 | "title", |
| 178 | 180 | Spec { |
| 179 | 181 | size: [fill(), px(ui::shell::TOOL)], |
| 180 | text: Some("Notebooks"), | |
| 182 | text: Some("My Notebooks"), | |
| 181 | 183 | color: Some(theme.text_dim), |
| 182 | 184 | ..Spec::default() |
| 183 | 185 | }, |
| 184 | 186 | ); |
| 185 | if right { | |
| 186 | toggle(ui); | |
| 187 | } | |
| 187 | // Room for what floats over the header's end. | |
| 188 | let end = if right { RAIL } else { NAV + MARGIN }; | |
| 189 | ui.leaf( | |
| 190 | "end", | |
| 191 | Spec { | |
| 192 | size: [px(end - MARGIN - 2.0 * ROW_PAD), px(1.0)], | |
| 193 | ..Spec::default() | |
| 194 | }, | |
| 195 | ); | |
| 188 | 196 | ui.close(); |
| 189 | 197 | if !rows { |
| 190 | 198 | return; |
| ... | ... | @@ -195,7 +203,7 @@ fn sidebar( |
| 195 | 203 | flags: Flags::SCROLL | Flags::CLIP, |
| 196 | 204 | axis: Axis::Y, |
| 197 | 205 | size: [fill(), fill()], |
| 198 | pad: [4.0, 2.0], | |
| 206 | pad: [MARGIN, 2.0], | |
| 199 | 207 | ..Spec::default() |
| 200 | 208 | }, |
| 201 | 209 | ); |
| ... | ... | @@ -248,7 +256,7 @@ fn sidebar( |
| 248 | 256 | Spec { |
| 249 | 257 | axis: Axis::Y, |
| 250 | 258 | size: [fill(), ui::children()], |
| 251 | pad: [4.0, 6.0], | |
| 259 | pad: [MARGIN, 6.0], | |
| 252 | 260 | ..Spec::default() |
| 253 | 261 | }, |
| 254 | 262 | ); |
| ... | ... | @@ -648,9 +656,13 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) { |
| 648 | 656 | fill: lit, |
| 649 | 657 | hover_fill: Some(ui::mix(lit.unwrap_or(theme.sidebar), theme.hover(), 0.6)), |
| 650 | 658 | radius: 4.0, |
| 651 | pad: [6.0 + INDENT * row.depth as f32, 0.0], | |
| 659 | pad: [ROW_PAD + INDENT * row.depth as f32, 0.0], | |
| 652 | 660 | // The field's text stands where the label did. |
| 653 | gap: if row.renamed { 6.0 - rename::PAD } else { 6.0 }, | |
| 661 | gap: if row.renamed { | |
| 662 | ROW_PAD - rename::PAD | |
| 663 | } else { | |
| 664 | ROW_PAD | |
| 665 | }, | |
| 654 | 666 | role: Some(accesskit::Role::TreeItem), |
| 655 | 667 | ..Spec::default() |
| 656 | 668 | }, |
| ... | ... | @@ -670,7 +682,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) { |
| 670 | 682 | ui.leaf( |
| 671 | 683 | "icon", |
| 672 | 684 | Spec { |
| 673 | size: [px(16.0), px(ROW)], | |
| 685 | size: [px(ICON), px(ROW)], | |
| 674 | 686 | icon: Some(icon), |
| 675 | 687 | color: Some([red, green, blue, alpha]), |
| 676 | 688 | ..Spec::default() |
| ... | ... | @@ -764,7 +776,7 @@ impl crate::State { |
| 764 | 776 | ..Spec::default() |
| 765 | 777 | }, |
| 766 | 778 | ); |
| 767 | crate::name(&mut self.ui, rows_id, "Notebooks"); | |
| 779 | crate::name(&mut self.ui, rows_id, "My Notebooks"); | |
| 768 | 780 | let shown = match (&self.session, &self.locked) { |
| 769 | 781 | (Some(session), _) => Some((&session.library, session.tabs[session.tab].path.as_str())), |
| 770 | 782 | (None, Some(locked)) => Some((&locked.library, locked.path.as_str())), |
| ... | ... | @@ -840,42 +852,71 @@ impl crate::State { |
| 840 | 852 | } |
| 841 | 853 | } |
| 842 | 854 | |
| 843 | /// The notebook button, floating at the body's corner over the section tabs' row, `height` | |
| 844 | /// tall as it eases, or over the sidebar's header while that is open. | |
| 845 | pub(crate) fn sidebar_button(&mut self, height: f32) { | |
| 855 | /// Back, Forward and the notebook button, floating at the body's corner over the section | |
| 856 | /// tabs' row, `height` tall as it eases. While the sidebar, `width` wide as it eases, is | |
| 857 | /// open on the left, the notebook button closes it from its header's icon, and Back and | |
| 858 | /// Forward ride its end. | |
| 859 | pub(crate) fn sidebar_button(&mut self, height: f32, width: f32) { | |
| 860 | use crate::commands::{Choice, Id as Cmd}; | |
| 846 | 861 | if self.temporary { |
| 847 | 862 | return; |
| 848 | 863 | } |
| 849 | // On the right it stands at the body's far edge, as laid out last frame. | |
| 850 | let x = match self.navigation_bar_right { | |
| 851 | true => self | |
| 864 | let (nav, toggle) = if self.navigation_bar_right { | |
| 865 | // The notebook button stands at the body's far edge, as laid out last frame. | |
| 866 | let edge = self | |
| 852 | 867 | .ui |
| 853 | 868 | .rect(self.ui.current()) |
| 854 | .map_or(0.0, |[left, _, right, _]| right - left - RAIL), | |
| 855 | false => 0.0, | |
| 869 | .map_or(0.0, |[left, _, right, _]| right - left - RAIL); | |
| 870 | (0.0, edge) | |
| 871 | } else { | |
| 872 | let open = MARGIN + ROW_PAD - (RAIL - ICON) / 2.0; | |
| 873 | let toggle = NAV + (open - NAV) * width / WIDTH; | |
| 874 | ((width - NAV - MARGIN).max(0.0), toggle) | |
| 856 | 875 | }; |
| 876 | let pad = [ | |
| 877 | (RAIL - ui::shell::TOOL) / 2.0, | |
| 878 | (RAIL - ui::shell::TOOL) / 2.0 + DROP, | |
| 879 | ]; | |
| 880 | self.ui.open( | |
| 881 | "back and forward", | |
| 882 | Spec { | |
| 883 | flags: Flags::FLOAT | Flags::CLIP, | |
| 884 | size: [px(NAV), px(height)], | |
| 885 | position: [nav, 0.0], | |
| 886 | pad, | |
| 887 | gap: 1.0, | |
| 888 | ..Spec::default() | |
| 889 | }, | |
| 890 | ); | |
| 891 | let format = self.format_state(); | |
| 892 | for id in [Cmd::Back, Cmd::Forward] { | |
| 893 | let status = self.status(&Choice::Command(id), &format); | |
| 894 | if let Some(choice) = crate::tool(&mut self.ui, id, status) { | |
| 895 | self.choose(choice); | |
| 896 | } | |
| 897 | } | |
| 898 | self.ui.close(); | |
| 857 | 899 | self.ui.open( |
| 858 | 900 | "toggle", |
| 859 | 901 | Spec { |
| 860 | 902 | flags: Flags::FLOAT | Flags::CLIP, |
| 861 | 903 | size: [px(RAIL), px(height)], |
| 862 | position: [x, 0.0], | |
| 863 | pad: [ | |
| 864 | (RAIL - ui::shell::TOOL) / 2.0, | |
| 865 | (RAIL - ui::shell::TOOL) / 2.0 + DROP, | |
| 866 | ], | |
| 904 | position: [toggle, 0.0], | |
| 905 | pad, | |
| 867 | 906 | ..Spec::default() |
| 868 | 907 | }, |
| 869 | 908 | ); |
| 870 | let tint = crate::notebook_color( | |
| 871 | &self.ui.theme, | |
| 872 | self.notebook().and_then(|library| library.color()), | |
| 873 | ); | |
| 874 | if ui::shell::tool_button(&mut self.ui, "button", art::NOTEBOOK, tint, None).clicked { | |
| 909 | let (icon, tint) = if self.sidebar { | |
| 910 | (art::CLOSE, self.ui.theme.text) | |
| 911 | } else { | |
| 912 | let color = self.notebook().and_then(|library| library.color()); | |
| 913 | (art::NOTEBOOK, crate::notebook_color(&self.ui.theme, color)) | |
| 914 | }; | |
| 915 | if ui::shell::tool_button(&mut self.ui, "button", icon, tint, None).clicked { | |
| 875 | 916 | self.sidebar = !self.sidebar; |
| 876 | 917 | self.save_settings(); |
| 877 | 918 | } |
| 878 | crate::tip(&mut self.ui, crate::commands::Id::Sidebar); | |
| 919 | crate::tip(&mut self.ui, Cmd::Sidebar); | |
| 879 | 920 | self.ui.close(); |
| 880 | 921 | } |
| 881 | 922 |
crates/snowbound/src/windows.rs+1-1| ... | ... | @@ -805,7 +805,7 @@ pub fn window_controls(ui: &mut Ui, window: &Window, end: bool) { |
| 805 | 805 | let size = if found { |
| 806 | 806 | [bounds.right - bounds.left, bounds.bottom - bounds.top].map(|side| side as f32 / scale) |
| 807 | 807 | } else { |
| 808 | [3.0 * CAPTION_BUTTON, crate::TITLE] | |
| 808 | [3.0 * CAPTION_BUTTON, crate::TOOLBAR] | |
| 809 | 809 | }; |
| 810 | 810 | let offset = [GAP, -(crate::TITLE - ui::shell::TOOL) / 2.0]; |
| 811 | 811 | if system && !eleven() { |
crates/snowbound/tests/replay.rs+1-1| ... | ... | @@ -364,7 +364,7 @@ fn renaming_a_section_from_its_tab_types_into_the_sidebar_at_once() { |
| 364 | 364 | let scratch = Scratch::new("rename-shut-sidebar"); |
| 365 | 365 | let notebook = |
| 366 | 366 | Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate"); |
| 367 | let mut steps = vec!["move 53 53", "press right", "release right", "settle"]; | |
| 367 | let mut steps = vec!["move 110 53", "press right", "release right", "settle"]; | |
| 368 | 368 | steps.extend(["key Down", "key Enter", "settle"]); |
| 369 | 369 | steps.extend(["type Renamed", "accessibility typed"]); |
| 370 | 370 | let [typed] = replay(&scratch, Some(&notebook), &steps) |
crates/ui/src/layout.rs+26-9| ... | ... | @@ -90,13 +90,9 @@ pub(crate) fn solve( |
| 90 | 90 | // goes back to the space. |
| 91 | 91 | let spare: f32 = space.iter().map(|child| nodes[*child].computed[axis]).sum(); |
| 92 | 92 | while excess > spare && axis == 0 { |
| 93 | let Some(group) = children | |
| 94 | .iter() | |
| 95 | .copied() | |
| 96 | .filter(|child| { | |
| 97 | nodes[*child].fold.is_some() && !nodes[nodes[*child].children[0]].hidden | |
| 98 | }) | |
| 99 | .min_by_key(|child| nodes[*child].fold) | |
| 93 | let mut groups = Vec::new(); | |
| 94 | unfolded(nodes, &children, &mut groups); | |
| 95 | let Some(group) = groups.into_iter().min_by_key(|group| nodes[*group].fold) | |
| 100 | 96 | else { |
| 101 | 97 | break; |
| 102 | 98 | }; |
| ... | ... | @@ -104,8 +100,14 @@ pub(crate) fn solve( |
| 104 | 100 | nodes[full].hidden = true; |
| 105 | 101 | nodes[folded].hidden = false; |
| 106 | 102 | let width = nodes[folded].computed[0] + 2.0 * nodes[group].pad[0]; |
| 107 | excess -= nodes[group].computed[0] - width; | |
| 108 | nodes[group].computed[0] = width; | |
| 103 | let freed = nodes[group].computed[0] - width; | |
| 104 | excess -= freed; | |
| 105 | // A group inside boxes sized by their children narrows them too. | |
| 106 | let mut inside = group; | |
| 107 | while inside != index { | |
| 108 | nodes[inside].computed[0] -= freed; | |
| 109 | inside = nodes[inside].parent; | |
| 110 | } | |
| 109 | 111 | } |
| 110 | 112 | excess = give_back(nodes, &space, axis, excess); |
| 111 | 113 | let mut tiers: Vec<f32> = sized |
| ... | ... | @@ -313,6 +315,21 @@ fn in_flow(nodes: &[Built], index: usize) -> impl Iterator<Item = usize> + '_ { |
| 313 | 315 | }) |
| 314 | 316 | } |
| 315 | 317 | |
| 318 | /// The groups among `boxes` still in their full form, and those inside any of them that is a | |
| 319 | /// row sized by its children, whose width follows theirs. | |
| 320 | fn unfolded(nodes: &[Built], boxes: &[usize], groups: &mut Vec<usize>) { | |
| 321 | for &child in boxes { | |
| 322 | let node = &nodes[child]; | |
| 323 | if node.fold.is_some() { | |
| 324 | if !nodes[node.children[0]].hidden { | |
| 325 | groups.push(child); | |
| 326 | } | |
| 327 | } else if node.size[0].size == Size::Children && node.axis == Axis::X { | |
| 328 | unfolded(nodes, &in_flow(nodes, child).collect::<Vec<_>>(), groups); | |
| 329 | } | |
| 330 | } | |
| 331 | } | |
| 332 | ||
| 316 | 333 | /// The children's extent on `axis`: summed with gaps along the flow, otherwise the largest. |
| 317 | 334 | fn flow(nodes: &[Built], index: usize, axis: usize) -> f32 { |
| 318 | 335 | let sizes = in_flow(nodes, index).map(|child| nodes[child].computed[axis]); |
crates/ui/src/lib.rs+2-1| ... | ... | @@ -390,7 +390,8 @@ pub struct Spec<'a> { |
| 390 | 390 | pub anchor: Option<Anchor>, |
| 391 | 391 | /// Makes the box a group of a row in two forms, its first two children, both built every |
| 392 | 392 | /// frame: its full form, and the one it folds to where the row lacks room. The row folds |
| 393 | /// its groups by ascending priority until it fits, once the space sized by ancestors has | |
| 393 | /// its groups, and those in rows of its own sized by their children, by ascending | |
| 394 | /// priority until it fits, once the space sized by ancestors has | |
| 394 | 395 | /// yielded and before any other box gives up room. The form not shown takes no room, |
| 395 | 396 | /// paint or input, and its boxes take the group's rectangle, so what opens from them |
| 396 | 397 | /// opens from the form shown. |
crates/ui/src/shell.rs+44| ... | ... | @@ -18,6 +18,8 @@ const TAB_ROUNDING: f32 = 4.0; |
| 18 | 18 | pub const TOOL: f32 = 22.0; |
| 19 | 19 | /// Width of a menu arrow beside a toolbar button. |
| 20 | 20 | const ARROW: f32 = 10.0; |
| 21 | /// Width of a button showing only a menu arrow, which ends its group as tight as an icon does. | |
| 22 | const MORE: f32 = 14.0; | |
| 21 | 23 | /// Where a section tab's label starts inside it. |
| 22 | 24 | pub const TAB_PAD: f32 = 10.0; |
| 23 | 25 | /// How far section tabs fade out at an end of their row they are cut at, and how near it a |
| ... | ... | @@ -483,6 +485,48 @@ pub fn split_button( |
| 483 | 485 | ui.signal(button) |
| 484 | 486 | } |
| 485 | 487 | |
| 488 | /// A narrow button showing only a menu arrow, which opens popup `menu` of more of a group's | |
| 489 | /// commands at the group's end; where not `enabled` it shows faded and takes no clicks. | |
| 490 | pub fn more_button(ui: &mut Ui, part: impl Hash, menu: Id, enabled: bool) -> Anchor { | |
| 491 | let theme = ui.theme.clone(); | |
| 492 | let open = ui.popup_open(menu); | |
| 493 | let id = ui.open( | |
| 494 | part, | |
| 495 | Spec { | |
| 496 | flags: if enabled { | |
| 497 | Flags::CLICKABLE | |
| 498 | } else { | |
| 499 | Flags::default() | |
| 500 | }, | |
| 501 | size: [px(MORE), px(TOOL)], | |
| 502 | icon: Some(CHEVRON), | |
| 503 | color: Some(if enabled { | |
| 504 | theme.text_dim | |
| 505 | } else { | |
| 506 | faded(theme.text_dim) | |
| 507 | }), | |
| 508 | fill: open.then(|| theme.hover()).or(rest(&theme)), | |
| 509 | hover_fill: enabled.then(|| theme.hover()), | |
| 510 | radius: 4.0, | |
| 511 | center: true, | |
| 512 | role: Some(Role::Button), | |
| 513 | ..Spec::default() | |
| 514 | }, | |
| 515 | ); | |
| 516 | if let Some(node) = ui.access(id) { | |
| 517 | node.set_has_popup(HasPopup::Menu); | |
| 518 | node.set_expanded(open); | |
| 519 | if !enabled { | |
| 520 | node.set_disabled(); | |
| 521 | } | |
| 522 | } | |
| 523 | ui.close(); | |
| 524 | if enabled && ui.signal(id).pressed { | |
| 525 | ui.open_popup(menu); | |
| 526 | } | |
| 527 | Anchor::Below(ui.rect(id).unwrap_or_default()) | |
| 528 | } | |
| 529 | ||
| 486 | 530 | /// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle |
| 487 | 531 | /// lit while `on` as `tool_button`'s. Returns where the menu opens so its icons line up |
| 488 | 532 | /// under the button's. |
crates/ui/src/tests.rs+73| ... | ... | @@ -232,6 +232,79 @@ fn a_row_is_narrowest_with_every_group_folded_and_every_box_squeezed() { |
| 232 | 232 | assert_eq!(rects[4], Some([40.0, 0.0, 60.0, 20.0])); |
| 233 | 233 | } |
| 234 | 234 | |
| 235 | #[test] | |
| 236 | fn a_group_inside_a_box_sized_by_its_children_folds_with_the_row() { | |
| 237 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 238 | let build = |ui: &mut Ui, width: f32| { | |
| 239 | frame(ui, |ui| { | |
| 240 | ui.open( | |
| 241 | "row", | |
| 242 | Spec { | |
| 243 | size: [px(width), px(20.0)], | |
| 244 | ..Spec::default() | |
| 245 | }, | |
| 246 | ); | |
| 247 | ui.open("outer", Spec::default()); | |
| 248 | ui.leaf( | |
| 249 | "fixed", | |
| 250 | Spec { | |
| 251 | size: [px(50.0), px(20.0)], | |
| 252 | ..Spec::default() | |
| 253 | }, | |
| 254 | ); | |
| 255 | ui.open( | |
| 256 | "group", | |
| 257 | Spec { | |
| 258 | fold: Some(0), | |
| 259 | ..Spec::default() | |
| 260 | }, | |
| 261 | ); | |
| 262 | for (form, width) in [("full", 100.0), ("folded", 20.0)] { | |
| 263 | ui.open(form, Spec::default()); | |
| 264 | ui.leaf( | |
| 265 | "button", | |
| 266 | Spec { | |
| 267 | size: [px(width), px(20.0)], | |
| 268 | ..Spec::default() | |
| 269 | }, | |
| 270 | ); | |
| 271 | ui.close(); | |
| 272 | } | |
| 273 | ui.close(); | |
| 274 | ui.close(); | |
| 275 | ui.open( | |
| 276 | "later", | |
| 277 | Spec { | |
| 278 | fold: Some(1), | |
| 279 | ..Spec::default() | |
| 280 | }, | |
| 281 | ); | |
| 282 | for (form, width) in [("full", 30.0), ("folded", 30.0)] { | |
| 283 | ui.open(form, Spec::default()); | |
| 284 | ui.leaf( | |
| 285 | "button", | |
| 286 | Spec { | |
| 287 | size: [px(width), px(20.0)], | |
| 288 | ..Spec::default() | |
| 289 | }, | |
| 290 | ); | |
| 291 | ui.close(); | |
| 292 | } | |
| 293 | ui.close(); | |
| 294 | ui.close(); | |
| 295 | }); | |
| 296 | }; | |
| 297 | let [outer, after] = ["outer", "later"].map(|part| Id::ROOT.child("row").child(part)); | |
| 298 | build(&mut ui, 180.0); | |
| 299 | assert_eq!(ui.rect(outer), Some([0.0, 0.0, 150.0, 20.0])); | |
| 300 | // 50 + 100 + 30 overflows 179, so the group folds before the later one of higher priority, | |
| 301 | // and the box around it narrows to 70. | |
| 302 | build(&mut ui, 179.0); | |
| 303 | assert_eq!(ui.rect(outer), Some([0.0, 0.0, 70.0, 20.0])); | |
| 304 | assert_eq!(ui.rect(after), Some([70.0, 0.0, 100.0, 20.0])); | |
| 305 | assert_eq!(ui.narrowest(Id::ROOT.child("row")), Some(100.0)); | |
| 306 | } | |
| 307 | ||
| 235 | 308 | #[test] |
| 236 | 309 | fn a_box_sized_by_its_children_yields_what_they_yield() { |
| 237 | 310 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); |