authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 18:02:12-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 19:55:45-07:00
log1de0431d8bcf7c9e558be7762f0996e76eb899b2
treeb7f53d59c1576871771688cab411271525fda038
parent218d66206d78eb0a122f2a2ab226c0b7a232dcfd
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: the toolbar after Clover's review: one Clipboard menu, a whole text group, Back, Forward and zoom in the tab row, and My Notebooks

- 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.5

14 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,9 +31,12 @@ keyboard, the toolbar and the macOS menu bar all run commands from it.
31### macOS31### macOS
3232
33- The toolbar's row is the title bar. An empty `NSToolbar`, unified compact33- 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,34 from macOS 11, with the title hidden makes the title bar 38 pt, and AppKit
35 and AppKit places the traffic lights in it; a press in the row's gaps drags35 centres the traffic lights in it beside the row's buttons; the row ends 4 pt
36 the window. The title is still set, for the Window menu, Mission Control and36 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 VoiceOver. `--screenshot` paints the lights where the hidden window's AppKit40 VoiceOver. `--screenshot` paints the lights where the hidden window's AppKit
38 put them. The app draws the row as part of the same frame as the rest of the41 put them. The app draws the row as part of the same frame as the rest of the
39 chrome. Under the whole42 chrome. Under the whole
arc/ui.md+2-1
...@@ -81,7 +81,8 @@ frame N...@@ -81,7 +81,8 @@ frame N
81 pixels, by its label, as a fraction of an ancestor, or by its children. When81 pixels, by its label, as a fraction of an ancestor, or by its children. When
82 siblings overflow, space sized from an ancestor gives way first. Then a row82 siblings overflow, space sized from an ancestor gives way first. Then a row
83 *folds* its groups, boxes with a full and a folded form (`Spec::fold`), by83 *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 least84 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 strict first. That one knob covers most of what flexbox is usually needed86 strict first. That one knob covers most of what flexbox is usually needed
86 for. A box filling across a parent sized by its children stretches to what87 for. A box filling across a parent sized by its children stretches to what
87 its siblings make it, and a popup that isn't strict gives way to the window,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,8 +1,4 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <defs>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 <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#79c2ff"/><stop offset="1" stop-color="#1f7cf0"/></linearGradient>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"/>
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"/>
8</svg>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,7 +77,7 @@ pub fn cover_border_line(ui: &mut ui::Ui, width: f32) {
77 ui::Spec {77 ui::Spec {
78 flags: ui::Flags::FLOAT,78 flags: ui::Flags::FLOAT,
79 size: [ui::px(width), ui::px(1.0)],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 fill: Some(draw::srgb(grey, grey, grey)),81 fill: Some(draw::srgb(grey, grey, grey)),
82 ..ui::Spec::default()82 ..ui::Spec::default()
83 },83 },
...@@ -85,7 +85,8 @@ pub fn cover_border_line(ui: &mut ui::Ui, width: f32) {...@@ -85,7 +85,8 @@ pub fn cover_border_line(ui: &mut ui::Ui, width: f32) {
85}85}
8686
87/// `-mouseDownCanMoveWindow` for the content view: a textured window drags from any press87/// `-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 space88/// 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/// itself, through `-performWindowDragWithEvent:`.90/// itself, through `-performWindowDragWithEvent:`.
90pub extern "C" fn no_window_drags(_: &AnyObject, _: Sel) -> Bool {91pub extern "C" fn no_window_drags(_: &AnyObject, _: Sel) -> Bool {
91 Bool::NO92 Bool::NO
crates/snowbound/src/macos.rs+21-11
...@@ -459,7 +459,7 @@ pub fn install_backdrop(window: &Window) -> bool {...@@ -459,7 +459,7 @@ pub fn install_backdrop(window: &Window) -> bool {
459 // Before 10.10 AppKit has no materials; 10.6's textured window stands in, its gradient459 // Before 10.10 AppKit has no materials; 10.6's textured window stands in, its gradient
460 // running through the toolbar and the tab row to the notebook's frame.460 // running through the toolbar and the tab row to the notebook's frame.
461 let Some(effect) = AnyClass::get("NSVisualEffectView") else {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 let view = &*ns_view(window);464 let view = &*ns_view(window);
465 unsafe {465 unsafe {
...@@ -1068,15 +1068,27 @@ pub fn set_min_size(window: &Window, size: [f32; 2]) {...@@ -1068,15 +1068,27 @@ pub fn set_min_size(window: &Window, size: [f32; 2]) {
1068 window.set_min_inner_size(Some(winit::dpi::LogicalSize::new(size[0], size[1])));1068 window.set_min_inner_size(Some(winit::dpi::LogicalSize::new(size[0], size[1])));
1069}1069}
10701070
1071/// Zooms the window once the current event is handled: AppKit's zoom animation runs its1071/// Zooms or minimizes the window, as a double click on a title bar does, once the current
1072/// own loop, and started from inside winit's handler it would hold every resize until the1072/// event is handled: AppKit's zoom animation runs its own loop, and started from inside
1073/// end, stretching the last frame instead of drawing each step.1073/// winit's handler it would hold every resize until the end, stretching the last frame
1074/// instead of drawing each step.
1074pub fn zoom(window: &Window) {1075pub fn zoom(window: &Window) {
1075 let window = ns_window(window);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 unsafe {1088 unsafe {
1077 let _: () = msg_send![1089 let _: () = msg_send![
1078 &window,1090 &window,
1079 performSelector: sel!(zoom:),1091 performSelector: selector,
1080 withObject: std::ptr::null::<AnyObject>(),1092 withObject: std::ptr::null::<AnyObject>(),
1081 afterDelay: 0.0f641093 afterDelay: 0.0f64
1082 ];1094 ];
...@@ -1096,12 +1108,10 @@ pub fn install_text_input(window: &Window) {...@@ -1096,12 +1108,10 @@ pub fn install_text_input(window: &Window) {
1096 sel!(insertText:replacementRange:),1108 sel!(insertText:replacementRange:),
1097 insert_text as unsafe extern "C" fn(_, _, _, _),1109 insert_text as unsafe extern "C" fn(_, _, _, _),
1098 );1110 );
1099 if crate::aqua::before_lion() {1111 class.add_method(
1100 class.add_method(1112 sel!(mouseDownCanMoveWindow),
1101 sel!(mouseDownCanMoveWindow),1113 crate::aqua::no_window_drags as extern "C" fn(_, _) -> _,
1102 crate::aqua::no_window_drags as extern "C" fn(_, _) -> _,1114 );
1103 );
1104 }
1105 class.register()1115 class.register()
1106 });1116 });
1107 assert_eq!(class.superclass(), Some(view.class()));1117 assert_eq!(class.superclass(), Some(view.class()));
crates/snowbound/src/main.rs+293-210
...@@ -134,9 +134,13 @@ use winit::{...@@ -134,9 +134,13 @@ use winit::{
134 window::CursorIcon,134 window::CursorIcon,
135};135};
136136
137/// Height of the toolbar's row, which is the title bar where the platform lets it: a137/// Height of the title bar the toolbar's buttons centre in: a unified compact toolbar's on
138/// unified compact toolbar's on macOS.138/// macOS, whose traffic lights they line up with.
139const TITLE: f32 = 38.0;139const 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.
143const TOOLBAR: f32 = TITLE - 4.0;
140/// Space between toolbar groups.144/// Space between toolbar groups.
141const GAP: f32 = 6.0;145const GAP: f32 = 6.0;
142/// A tool face's room beside its buttons.146/// A tool face's room beside its buttons.
...@@ -898,6 +902,8 @@ struct State {...@@ -898,6 +902,8 @@ struct State {
898 strip_press: Option<Instant>,902 strip_press: Option<Instant>,
899 /// The strip is held and the window moves once the pointer does.903 /// The strip is held and the window moves once the pointer does.
900 strip_held: bool,904 strip_held: bool,
905 /// The zoom typed in the zoom's field while it is open.
906 zoom_typed: Option<String>,
901 /// Where a replay asked the next frame to be written.907 /// Where a replay asked the next frame to be written.
902 snapshot: Option<PathBuf>,908 snapshot: Option<PathBuf>,
903 /// A replay waiting for nothing to be on its way, and where it wants the accessibility909 /// A replay waiting for nothing to be on its way, and where it wants the accessibility
...@@ -1247,6 +1253,7 @@ impl State {...@@ -1247,6 +1253,7 @@ impl State {
1247 pointer: [0.0; 2],1253 pointer: [0.0; 2],
1248 strip_press: None,1254 strip_press: None,
1249 strip_held: false,1255 strip_held: false,
1256 zoom_typed: None,
1250 snapshot: None,1257 snapshot: None,
1251 replay_settle: None,1258 replay_settle: None,
1252 frames: std::env::var_os("SNOWBOUND_FRAMES").map(|dir| (dir.into(), Instant::now())),1259 frames: std::env::var_os("SNOWBOUND_FRAMES").map(|dir| (dir.into(), Instant::now())),
...@@ -1514,7 +1521,7 @@ impl State {...@@ -1514,7 +1521,7 @@ impl State {
1514 if !self.navigation_bar_right {1521 if !self.navigation_bar_right {
1515 self.sidebar(&theme, sidebar);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 let beside = 1.0 - sidebar / sidebar::WIDTH;1525 let beside = 1.0 - sidebar / sidebar::WIDTH;
1519 self.ui.open(1526 self.ui.open(
1520 "main",1527 "main",
...@@ -1544,14 +1551,19 @@ impl State {...@@ -1544,14 +1551,19 @@ impl State {
1544 );1551 );
1545 if !self.temporary {1552 if !self.temporary {
1546 // Past the row's padding, the frame's corner and the first tab's shadow, the tabs'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's1554 // outlines start where Back and Forward end, and on the left the notebook button's
1548 // easing, so the tabs ease with it.1555 // square after them; that room shrinks on the sidebar's easing, as they move into
1549 let room = TAB_ROW - FRAME - self.rounding() - ui::SHADOW[0];1556 // its header, so the tabs ease with it.
1550 let room = if self.navigation_bar_right { 0.0 } else { room };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 self.ui.leaf(1563 self.ui.leaf(
1552 "rail",1564 "rail",
1553 Spec {1565 Spec {
1554 size: [px(room * beside), px(1.0)],1566 size: [px(room), px(1.0)],
1555 ..Spec::default()1567 ..Spec::default()
1556 },1568 },
1557 );1569 );
...@@ -1675,6 +1687,7 @@ impl State {...@@ -1675,6 +1687,7 @@ impl State {
1675 #[cfg(feature = "live")]1687 #[cfg(feature = "live")]
1676 self.avatars();1688 self.avatars();
1677 if self.session.is_some() {1689 if self.session.is_some() {
1690 self.zoom(&theme)?;
1678 self.page_tools(&theme);1691 self.page_tools(&theme);
1679 }1692 }
1680 let button = if self.navigation_bar_right && !self.temporary {1693 let button = if self.navigation_bar_right && !self.temporary {
...@@ -1737,7 +1750,7 @@ impl State {...@@ -1737,7 +1750,7 @@ impl State {
1737 if self.navigation_bar_right {1750 if self.navigation_bar_right {
1738 self.sidebar(&theme, sidebar);1751 self.sidebar(&theme, sidebar);
1739 }1752 }
1740 self.sidebar_button(height);1753 self.sidebar_button(height, sidebar);
1741 self.ui.close();1754 self.ui.close();
1742 self.context_menu();1755 self.context_menu();
1743 return Ok((section, open_tab, None));1756 return Ok((section, open_tab, None));
...@@ -1750,7 +1763,7 @@ impl State {...@@ -1750,7 +1763,7 @@ impl State {
1750 if self.navigation_bar_right {1763 if self.navigation_bar_right {
1751 self.sidebar(&theme, sidebar);1764 self.sidebar(&theme, sidebar);
1752 }1765 }
1753 self.sidebar_button(height);1766 self.sidebar_button(height, sidebar);
1754 self.ui.close();1767 self.ui.close();
1755 self.context_menu();1768 self.context_menu();
1756 return Ok((section, open_tab, None));1769 return Ok((section, open_tab, None));
...@@ -1836,7 +1849,7 @@ impl State {...@@ -1836,7 +1849,7 @@ impl State {
1836 self.sidebar(&theme, sidebar);1849 self.sidebar(&theme, sidebar);
1837 }1850 }
1838 self.task_pane(&theme);1851 self.task_pane(&theme);
1839 self.sidebar_button(height);1852 self.sidebar_button(height, sidebar);
1840 self.ui.close();1853 self.ui.close();
1841 self.context_menu();1854 self.context_menu();
1842 // No tab stands on the frame in Full Page View.1855 // No tab stands on the frame in Full Page View.
...@@ -2025,7 +2038,7 @@ impl State {...@@ -2025,7 +2038,7 @@ impl State {
2025 } else {2038 } else {
2026 Flags::default()2039 Flags::default()
2027 },2040 },
2028 size: [fill(), px(TITLE)],2041 size: [fill(), px(TOOLBAR)],
2029 pad: [0.0, (TITLE - ui::shell::TOOL) / 2.0],2042 pad: [0.0, (TITLE - ui::shell::TOOL) / 2.0],
2030 gap: GAP,2043 gap: GAP,
2031 role: Some(accesskit::Role::Toolbar),2044 role: Some(accesskit::Role::Toolbar),
...@@ -2042,7 +2055,7 @@ impl State {...@@ -2042,7 +2055,7 @@ impl State {
2042 self.ui.open(2055 self.ui.open(
2043 "bar",2056 "bar",
2044 Spec {2057 Spec {
2045 size: [fill(), px(TITLE)],2058 size: [fill(), px(TOOLBAR)],
2046 fill: Some(theme.strip),2059 fill: Some(theme.strip),
2047 pad: [0.0, edge],2060 pad: [0.0, edge],
2048 offset: [0.0, -edge],2061 offset: [0.0, -edge],
...@@ -2070,7 +2083,7 @@ impl State {...@@ -2070,7 +2083,7 @@ impl State {
2070 "tools",2083 "tools",
2071 Spec {2084 Spec {
2072 flags: Flags::CLIP,2085 flags: Flags::CLIP,
2073 size: [fill(), px(TITLE)],2086 size: [fill(), px(TOOLBAR)],
2074 pad: [face.pad[0], edge],2087 pad: [face.pad[0], edge],
2075 offset: [0.0, -edge],2088 offset: [0.0, -edge],
2076 gap: if theme.tool[3] > 0.0 && !panel {2089 gap: if theme.tool[3] > 0.0 && !panel {
...@@ -2121,7 +2134,6 @@ impl State {...@@ -2121,7 +2134,6 @@ impl State {
2121 use Entry::{Open, Rule, Run};2134 use Entry::{Open, Rule, Run};
2122 use canvas::editor::{Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, Toggle};2135 use canvas::editor::{Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, Toggle};
2123 use commands::{Choice, Id as Cmd};2136 use commands::{Choice, Id as Cmd};
2124 use ui::shell::TOOL;
2125 // A focused picture has no text to show a format for.2137 // A focused picture has no text to show a format for.
2126 let state = self.format_state();2138 let state = self.format_state();
2127 let statuses: Vec<_> = commands::COMMANDS2139 let statuses: Vec<_> = commands::COMMANDS
...@@ -2145,90 +2157,73 @@ impl State {...@@ -2145,90 +2157,73 @@ impl State {
2145 let engine = &self.view.engine;2157 let engine = &self.view.engine;
2146 let session = self.session.as_ref();2158 let session = self.session.as_ref();
2147 let update = self.updates.status();2159 let update = self.updates.status();
2148 let label = format!("{:.0}%", self.view.zoom() * 100.0);
2149 let sheet = self.gallery_sheet();2160 let sheet = self.gallery_sheet();
2150 let drawing_pens = self.pens();2161 let drawing_pens = self.pens();
2151 let ui = &mut self.ui;2162 let ui = &mut self.ui;
2152 let text = theme.text;2163 let text = theme.text;
2153 let tags: Vec<_> = self.tags.iter().enumerate().collect();2164 let tags: Vec<_> = self.tags.iter().enumerate().collect();
2165 let face = tool_face(theme, grouped(theme), FACE_RISE);
2154 let mut choice = group(2166 let mut choice = group(
2155 ui,2167 ui,
2156 "navigate",2168 "navigate",
2157 6,2169 6,
2170 face.clone(),
2158 |ui| {2171 |ui| {
2159 let mut choice = None;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 choice = tool(ui, id, status_of(id)).or(choice);2174 choice = tool(ui, id, status_of(id)).or(choice);
2162 }2175 }
2163 choice2176 choice
2164 },2177 },
2165 |ui| {2178 |ui| {
2166 let entries = [Run(Cmd::Redo), Rule, Run(Cmd::Back), Run(Cmd::Forward)];2179 dropdown(
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(
2181 ui,2180 ui,
2182 "paste",2181 "menu",
2183 title(Cmd::Paste),2182 Head::Split(Cmd::Undo),
2184 art::PASTE,2183 &[Run(Cmd::Redo)],
2185 None,2184 status_of,
2186 None,
2187 toolbar_popup("paste"),
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| {2187 );
2221 let entries = [2188 ui.open(
2222 Run(Cmd::Cut),2189 "clipboard",
2223 Run(Cmd::Copy),2190 Spec {
2224 Run(Cmd::FormatPainter),2191 flags: Flags::CLICKABLE,
2225 Rule,2192 ..face.clone()
2226 Open("paste", Cmd::Paste),
2227 ];
2228 dropdown(ui, "menu", Head::Split(Cmd::Paste), &entries, status_of)
2229 },2193 },
2230 )2194 );
2231 .or(choice);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 let font = state.font.clone().unwrap_or_default();2227 let font = state.font.clone().unwrap_or_default();
2233 let size = state2228 let size = state
2234 .font_size2229 .font_size
...@@ -2236,12 +2231,13 @@ impl State {...@@ -2236,12 +2231,13 @@ impl State {
2236 ui.open(2231 ui.open(
2237 "font",2232 "font",
2238 Spec {2233 Spec {
2234 flags: Flags::CLICKABLE,
2239 gap: 1.0,2235 gap: 1.0,
2240 ..tool_face(theme, grouped(theme), FACE_RISE)2236 ..face.clone()
2241 },2237 },
2242 );2238 );
2243 divider(ui, theme);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 let styles = toolbar_popup("styles");2241 let styles = toolbar_popup("styles");
2246 let anchor = if status_of(Cmd::Styles).enabled {2242 let anchor = if status_of(Cmd::Styles).enabled {
2247 ui::shell::menu_button(ui, "styles", art::STYLES, None, styles)2243 ui::shell::menu_button(ui, "styles", art::STYLES, None, styles)
...@@ -2342,14 +2338,22 @@ impl State {...@@ -2342,14 +2338,22 @@ impl State {
2342 let id = Cmd::Toggle(toggle);2338 let id = Cmd::Toggle(toggle);
2343 choice = tool(ui, id, status_of(id)).or(choice);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 choice = group(2349 choice = group(
2347 ui,2350 ui,
2348 "character",2351 "character",
2349 8,2352 8,
2353 Spec::default(),
2350 |ui| {2354 |ui| {
2351 divider(ui, theme);2355 let head = Head::Menu("Text Effects", art::STRIKETHROUGH);
2352 let mut choice = None;2356 let mut choice = dropdown(ui, "script", head, &effects, status_of);
2353 // Each button applies its menu's last pick: the highlighter shows it in its2357 // Each button applies its menu's last pick: the highlighter shows it in its
2354 // artwork, the font colour in a bar.2358 // artwork, the font colour in a bar.
2355 for (part, id, swatches, columns, none, bar) in [2359 for (part, id, swatches, columns, none, bar) in [
...@@ -2410,37 +2414,23 @@ impl State {...@@ -2410,37 +2414,23 @@ impl State {
2410 });2414 });
2411 }2415 }
2412 }2416 }
2413 dropdown(2417 tool(ui, Cmd::FormatPainter, status_of(Cmd::FormatPainter)).or(choice)
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)
2427 },2418 },
2428 |ui| {2419 |ui| {
2429 let entries = [2420 let mut entries = effects.to_vec();
2421 entries.extend([
2422 Rule,
2430 Open("highlight", Cmd::Highlight),2423 Open("highlight", Cmd::Highlight),
2431 Run(Cmd::FontColor),2424 Run(Cmd::FontColor),
2432 Open("color", Cmd::FontColor),2425 Open("color", Cmd::FontColor),
2433 Rule,2426 Rule,
2434 Run(Cmd::Toggle(Toggle::Strikethrough)),2427 Run(Cmd::FormatPainter),
2435 Run(Cmd::Toggle(Toggle::Subscript)),2428 ]);
2436 Run(Cmd::Toggle(Toggle::Superscript)),
2437 Rule,
2438 Run(Cmd::ClearFormatting),
2439 ];
2440 dropdown(ui, "menu", Head::Split(Cmd::Highlight), &entries, status_of)2429 dropdown(ui, "menu", Head::Split(Cmd::Highlight), &entries, status_of)
2441 },2430 },
2442 )2431 )
2443 .or(choice);2432 .or(choice);
2433 ui.close();
2444 let alignments = [Alignment::Left, Alignment::Center, Alignment::Right].map(Cmd::Align);2434 let alignments = [Alignment::Left, Alignment::Center, Alignment::Right].map(Cmd::Align);
2445 let paragraph = [2435 let paragraph = [
2446 Rule,2436 Rule,
...@@ -2455,6 +2445,7 @@ impl State {...@@ -2455,6 +2445,7 @@ impl State {
2455 ui,2445 ui,
2456 "paragraph",2446 "paragraph",
2457 7,2447 7,
2448 face.clone(),
2458 |ui| {2449 |ui| {
2459 divider(ui, theme);2450 divider(ui, theme);
2460 let mut choice = None;2451 let mut choice = None;
...@@ -2603,6 +2594,7 @@ impl State {...@@ -2603,6 +2594,7 @@ impl State {
2603 ui,2594 ui,
2604 "tags",2595 "tags",
2605 9,2596 9,
2597 face.clone(),
2606 |ui| {2598 |ui| {
2607 divider(ui, theme);2599 divider(ui, theme);
2608 let mut choice = None;2600 let mut choice = None;
...@@ -2659,6 +2651,7 @@ impl State {...@@ -2659,6 +2651,7 @@ impl State {
2659 ui,2651 ui,
2660 "insert",2652 "insert",
2661 4,2653 4,
2654 face.clone(),
2662 |ui| {2655 |ui| {
2663 divider(ui, theme);2656 divider(ui, theme);
2664 let mut choice = None;2657 let mut choice = None;
...@@ -2742,6 +2735,7 @@ impl State {...@@ -2742,6 +2735,7 @@ impl State {
2742 ui,2735 ui,
2743 part,2736 part,
2744 priority,2737 priority,
2738 face.clone(),
2745 |ui| {2739 |ui| {
2746 divider(ui, theme);2740 divider(ui, theme);
2747 let mut choice = None;2741 let mut choice = None;
...@@ -2760,6 +2754,7 @@ impl State {...@@ -2760,6 +2754,7 @@ impl State {
2760 ui,2754 ui,
2761 "draw",2755 "draw",
2762 2,2756 2,
2757 face.clone(),
2763 |ui| {2758 |ui| {
2764 divider(ui, theme);2759 divider(ui, theme);
2765 let mut choice = tool(ui, Cmd::SelectType, status_of(Cmd::SelectType));2760 let mut choice = tool(ui, Cmd::SelectType, status_of(Cmd::SelectType));
...@@ -2805,6 +2800,40 @@ impl State {...@@ -2805,6 +2800,40 @@ impl State {
2805 |_| None,2800 |_| None,
2806 )2801 )
2807 .or(choice);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 ui.leaf(2837 ui.leaf(
2809 "space",2838 "space",
2810 Spec {2839 Spec {
...@@ -2815,7 +2844,14 @@ impl State {...@@ -2815,7 +2844,14 @@ impl State {
2815 if let Some(session) = session {2844 if let Some(session) = session {
2816 sync::control(ui, session, &update, theme);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 let views = [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 Run(Cmd::Sidebar),2855 Run(Cmd::Sidebar),
2820 Run(Cmd::PageList),2856 Run(Cmd::PageList),
2821 Run(Cmd::PagesMatchTheme),2857 Run(Cmd::PagesMatchTheme),
...@@ -2824,95 +2860,151 @@ impl State {...@@ -2824,95 +2860,151 @@ impl State {
2824 Run(Cmd::HideSpelling),2860 Run(Cmd::HideSpelling),
2825 Run(Cmd::Spelling),2861 Run(Cmd::Spelling),
2826 ];2862 ];
2827 choice = group(2863 ui.open(
2828 ui,
2829 "view",2864 "view",
2830 1,2865 Spec {
2831 |ui| {2866 flags: Flags::CLICKABLE,
2832 divider(ui, theme);2867 ..face.clone()
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)
2892 },2868 },
2893 |ui| {2869 );
2894 let mut entries = vec![2870 let head = Head::More("More View Options");
2895 Run(Cmd::ZoomIn),2871 choice = dropdown(ui, "more", head, &views, status_of).or(choice);
2896 Run(Cmd::ZoomOut),2872 ui.close();
2897 Run(Cmd::ActualSize),2873 if let Some(choice) = choice {
2898 Rule,2874 self.choose(choice);
2899 Open("page color", Cmd::PageColor),2875 }
2900 Rule,2876 }
2901 ];2877
2902 entries.extend(views);2878 /// The page's zoom, left of the search box: Zoom Out, the level and Zoom In joined as one
2903 dropdown(2879 /// control, which folds away first where the tab row lacks room. Clicking the level opens
2904 ui,2880 /// it as a field, where Enter zooms to the percentage typed and Escape keeps the zoom.
2905 "menu",2881 fn zoom(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> {
2906 Head::Menu("View", art::ZOOM_IN),2882 use commands::{Choice, Id as Cmd};
2907 &entries,2883 use ui::shell::TOOL;
2908 status_of,2884 const LEVEL: f32 = 44.0;
2909 )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 )2900 );
2912 .or(choice);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 if let Some(choice) = choice {3000 if let Some(choice) = choice {
2914 self.choose(choice);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 }
29173009
2918 /// The search box and the page list's buttons, above the list. Where the tab row lacks3010 /// The search box and the page list's buttons, above the list. Where the tab row lacks
...@@ -2920,10 +3012,12 @@ impl State {...@@ -2920,10 +3012,12 @@ impl State {
2920 /// on the page it stays.3012 /// on the page it stays.
2921 fn page_tools(&mut self, theme: &Theme) {3013 fn page_tools(&mut self, theme: &Theme) {
2922 let finding = self.search.finding;3014 let finding = self.search.finding;
3015 // It folds after the zoom.
2923 self.ui.open(3016 self.ui.open(
2924 "tools",3017 "tools",
2925 Spec {3018 Spec {
2926 fold: (!finding).then_some(0),3019 flags: Flags::CLICKABLE,
3020 fold: (!finding).then_some(1),
2927 ..Spec::default()3021 ..Spec::default()
2928 },3022 },
2929 );3023 );
...@@ -5131,19 +5225,21 @@ fn pen_gallery(...@@ -5131,19 +5225,21 @@ fn pen_gallery(
5131 )5225 )
5132}5226}
51335227
5134/// A toolbar group that folds by `priority`: `full` builds its full form, `folded` the form5228/// A toolbar group on `face` that folds by `priority`: `full` builds its full form, `folded`
5135/// it folds to. Returns what either chose.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.
5136fn group(5231fn group(
5137 ui: &mut Ui,5232 ui: &mut Ui,
5138 part: &str,5233 part: &str,
5139 priority: u32,5234 priority: u32,
5235 face: Spec<'static>,
5140 full: impl FnOnce(&mut Ui) -> Option<commands::Choice>,5236 full: impl FnOnce(&mut Ui) -> Option<commands::Choice>,
5141 folded: impl FnOnce(&mut Ui) -> Option<commands::Choice>,5237 folded: impl FnOnce(&mut Ui) -> Option<commands::Choice>,
5142) -> Option<commands::Choice> {5238) -> Option<commands::Choice> {
5143 let face = tool_face(&ui.theme, grouped(&ui.theme), FACE_RISE);
5144 ui.open(5239 ui.open(
5145 part,5240 part,
5146 Spec {5241 Spec {
5242 flags: Flags::CLICKABLE,
5147 fold: Some(priority),5243 fold: Some(priority),
5148 ..face5244 ..face
5149 },5245 },
...@@ -5239,13 +5335,9 @@ fn dropdown(...@@ -5239,13 +5335,9 @@ fn dropdown(
5239 anchor5335 anchor
5240 }5336 }
5241 // A menu of nothing that applies fades as a button would.5337 // A menu of nothing that applies fades as a button would.
5242 Head::Menu(label, _) | Head::Tools(label, _) | Head::More(label) if !usable => {5338 Head::Menu(label, icon) | Head::Tools(label, icon) if !usable => {
5243 let (icon, arrow) = match head {
5244 Head::Menu(_, icon) | Head::Tools(_, icon) => (icon, true),
5245 _ => (ui::shell::CHEVRON, false),
5246 };
5247 let tint = ui.theme.text;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 name(ui, button, label);5341 name(ui, button, label);
5250 ui::Anchor::Below(ui.rect(button).unwrap_or_default())5342 ui::Anchor::Below(ui.rect(button).unwrap_or_default())
5251 }5343 }
...@@ -5264,18 +5356,9 @@ fn dropdown(...@@ -5264,18 +5356,9 @@ fn dropdown(
5264 anchor5356 anchor
5265 }5357 }
5266 Head::More(label) => {5358 Head::More(label) => {
5267 let open = ui.popup_open(menu);5359 let anchor = ui::shell::more_button(ui, part, menu, usable);
5268 let text = ui.theme.text;5360 name(ui, button, label);
5269 if ui::shell::tool_button(ui, part, ui::shell::CHEVRON, text, Some(open)).pressed {5361 anchor
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())
5279 }5362 }
5280 };5363 };
5281 let keys: Vec<_> = entries5364 let keys: Vec<_> = entries
crates/snowbound/src/screenshot/snow_leopard.rs+1-1
...@@ -181,7 +181,7 @@ fn window(...@@ -181,7 +181,7 @@ fn window(
181) -> Result<Vec<[f32; 4]>, Box<dyn Error>> {181) -> Result<Vec<[f32; 4]>, Box<dyn Error>> {
182 let [highlight, top, bottom, body] = GREYS[usize::from(!key)];182 let [highlight, top, bottom, body] = GREYS[usize::from(!key)];
183 // The gradient runs from under the highlight to the top content border's last row.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 let total = height + TITLE_LINE;185 let total = height + TITLE_LINE;
186 let mut pixels: Vec<[f32; 4]> = (0..total)186 let mut pixels: Vec<[f32; 4]> = (0..total)
187 .flat_map(|y| {187 .flat_map(|y| {
crates/snowbound/src/sidebar.rs+86-45
...@@ -18,6 +18,13 @@ const RAIL: f32 = crate::TAB_ROW;...@@ -18,6 +18,13 @@ const RAIL: f32 = crate::TAB_ROW;
18/// on the row's foot, so their labels sit 1 to 3 pixels low.18/// on the row's foot, so their labels sit 1 to 3 pixels low.
19const DROP: f32 = 2.0;19const DROP: f32 = 2.0;
20const ROW: f32 = 24.0;20const 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.
23const MARGIN: f32 = 4.0;
24const ROW_PAD: f32 = 6.0;
25const ICON: f32 = 16.0;
26/// The Back and Forward buttons' box, with room beside them as the notebook button's square has.
27pub const NAV: f32 = 2.0 * ui::shell::TOOL + 1.0 + (RAIL - ui::shell::TOOL);
21/// How far each level of the tree sits inside its parent.28/// How far each level of the tree sits inside its parent.
22const INDENT: f32 = 16.0;29const INDENT: f32 = 16.0;
2330
...@@ -136,7 +143,8 @@ pub fn header() -> Id {...@@ -136,7 +143,8 @@ pub fn header() -> Id {
136143
137/// The sidebar's header row, as tall as the tab row and dragging the window when `drags`, then144/// The sidebar's header row, as tall as the tab row and dragging the window when `drags`, then
138/// with `rows` the tree of `notebooks` with the open section marked. The notebook button145/// 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.
140fn sidebar(148fn sidebar(
141 ui: &mut Ui,149 ui: &mut Ui,
142 tree: &mut Tree,150 tree: &mut Tree,
...@@ -155,36 +163,36 @@ fn sidebar(...@@ -155,36 +163,36 @@ fn sidebar(
155 Flags::default()163 Flags::default()
156 },164 },
157 size: [fill(), px(RAIL)],165 size: [fill(), px(RAIL)],
158 pad: [crate::FRAME, (RAIL - ui::shell::TOOL) / 2.0 + DROP],166 pad: [MARGIN + ROW_PAD, (RAIL - ui::shell::TOOL) / 2.0 + DROP],
159 gap: 6.0,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 ..Spec::default()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 ui.leaf(178 ui.leaf(
177 "title",179 "title",
178 Spec {180 Spec {
179 size: [fill(), px(ui::shell::TOOL)],181 size: [fill(), px(ui::shell::TOOL)],
180 text: Some("Notebooks"),182 text: Some("My Notebooks"),
181 color: Some(theme.text_dim),183 color: Some(theme.text_dim),
182 ..Spec::default()184 ..Spec::default()
183 },185 },
184 );186 );
185 if right {187 // Room for what floats over the header's end.
186 toggle(ui);188 let end = if right { RAIL } else { NAV + MARGIN };
187 }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 ui.close();196 ui.close();
189 if !rows {197 if !rows {
190 return;198 return;
...@@ -195,7 +203,7 @@ fn sidebar(...@@ -195,7 +203,7 @@ fn sidebar(
195 flags: Flags::SCROLL | Flags::CLIP,203 flags: Flags::SCROLL | Flags::CLIP,
196 axis: Axis::Y,204 axis: Axis::Y,
197 size: [fill(), fill()],205 size: [fill(), fill()],
198 pad: [4.0, 2.0],206 pad: [MARGIN, 2.0],
199 ..Spec::default()207 ..Spec::default()
200 },208 },
201 );209 );
...@@ -248,7 +256,7 @@ fn sidebar(...@@ -248,7 +256,7 @@ fn sidebar(
248 Spec {256 Spec {
249 axis: Axis::Y,257 axis: Axis::Y,
250 size: [fill(), ui::children()],258 size: [fill(), ui::children()],
251 pad: [4.0, 6.0],259 pad: [MARGIN, 6.0],
252 ..Spec::default()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,9 +656,13 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) {
648 fill: lit,656 fill: lit,
649 hover_fill: Some(ui::mix(lit.unwrap_or(theme.sidebar), theme.hover(), 0.6)),657 hover_fill: Some(ui::mix(lit.unwrap_or(theme.sidebar), theme.hover(), 0.6)),
650 radius: 4.0,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 // The field's text stands where the label did.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 role: Some(accesskit::Role::TreeItem),666 role: Some(accesskit::Role::TreeItem),
655 ..Spec::default()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,7 +682,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) {
670 ui.leaf(682 ui.leaf(
671 "icon",683 "icon",
672 Spec {684 Spec {
673 size: [px(16.0), px(ROW)],685 size: [px(ICON), px(ROW)],
674 icon: Some(icon),686 icon: Some(icon),
675 color: Some([red, green, blue, alpha]),687 color: Some([red, green, blue, alpha]),
676 ..Spec::default()688 ..Spec::default()
...@@ -764,7 +776,7 @@ impl crate::State {...@@ -764,7 +776,7 @@ impl crate::State {
764 ..Spec::default()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 let shown = match (&self.session, &self.locked) {780 let shown = match (&self.session, &self.locked) {
769 (Some(session), _) => Some((&session.library, session.tabs[session.tab].path.as_str())),781 (Some(session), _) => Some((&session.library, session.tabs[session.tab].path.as_str())),
770 (None, Some(locked)) => Some((&locked.library, locked.path.as_str())),782 (None, Some(locked)) => Some((&locked.library, locked.path.as_str())),
...@@ -840,42 +852,71 @@ impl crate::State {...@@ -840,42 +852,71 @@ impl crate::State {
840 }852 }
841 }853 }
842854
843 /// The notebook button, floating at the body's corner over the section tabs' row, `height`855 /// Back, Forward and the notebook button, floating at the body's corner over the section
844 /// tall as it eases, or over the sidebar's header while that is open.856 /// tabs' row, `height` tall as it eases. While the sidebar, `width` wide as it eases, is
845 pub(crate) fn sidebar_button(&mut self, height: f32) {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 if self.temporary {861 if self.temporary {
847 return;862 return;
848 }863 }
849 // On the right it stands at the body's far edge, as laid out last frame.864 let (nav, toggle) = if self.navigation_bar_right {
850 let x = match self.navigation_bar_right {865 // The notebook button stands at the body's far edge, as laid out last frame.
851 true => self866 let edge = self
852 .ui867 .ui
853 .rect(self.ui.current())868 .rect(self.ui.current())
854 .map_or(0.0, |[left, _, right, _]| right - left - RAIL),869 .map_or(0.0, |[left, _, right, _]| right - left - RAIL);
855 false => 0.0,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 self.ui.open(899 self.ui.open(
858 "toggle",900 "toggle",
859 Spec {901 Spec {
860 flags: Flags::FLOAT | Flags::CLIP,902 flags: Flags::FLOAT | Flags::CLIP,
861 size: [px(RAIL), px(height)],903 size: [px(RAIL), px(height)],
862 position: [x, 0.0],904 position: [toggle, 0.0],
863 pad: [905 pad,
864 (RAIL - ui::shell::TOOL) / 2.0,
865 (RAIL - ui::shell::TOOL) / 2.0 + DROP,
866 ],
867 ..Spec::default()906 ..Spec::default()
868 },907 },
869 );908 );
870 let tint = crate::notebook_color(909 let (icon, tint) = if self.sidebar {
871 &self.ui.theme,910 (art::CLOSE, self.ui.theme.text)
872 self.notebook().and_then(|library| library.color()),911 } else {
873 );912 let color = self.notebook().and_then(|library| library.color());
874 if ui::shell::tool_button(&mut self.ui, "button", art::NOTEBOOK, tint, None).clicked {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 self.sidebar = !self.sidebar;916 self.sidebar = !self.sidebar;
876 self.save_settings();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 self.ui.close();920 self.ui.close();
880 }921 }
881922
crates/snowbound/src/windows.rs+1-1
...@@ -805,7 +805,7 @@ pub fn window_controls(ui: &mut Ui, window: &Window, end: bool) {...@@ -805,7 +805,7 @@ pub fn window_controls(ui: &mut Ui, window: &Window, end: bool) {
805 let size = if found {805 let size = if found {
806 [bounds.right - bounds.left, bounds.bottom - bounds.top].map(|side| side as f32 / scale)806 [bounds.right - bounds.left, bounds.bottom - bounds.top].map(|side| side as f32 / scale)
807 } else {807 } else {
808 [3.0 * CAPTION_BUTTON, crate::TITLE]808 [3.0 * CAPTION_BUTTON, crate::TOOLBAR]
809 };809 };
810 let offset = [GAP, -(crate::TITLE - ui::shell::TOOL) / 2.0];810 let offset = [GAP, -(crate::TITLE - ui::shell::TOOL) / 2.0];
811 if system && !eleven() {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,7 +364,7 @@ fn renaming_a_section_from_its_tab_types_into_the_sidebar_at_once() {
364 let scratch = Scratch::new("rename-shut-sidebar");364 let scratch = Scratch::new("rename-shut-sidebar");
365 let notebook =365 let notebook =
366 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate");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 steps.extend(["key Down", "key Enter", "settle"]);368 steps.extend(["key Down", "key Enter", "settle"]);
369 steps.extend(["type Renamed", "accessibility typed"]);369 steps.extend(["type Renamed", "accessibility typed"]);
370 let [typed] = replay(&scratch, Some(&notebook), &steps)370 let [typed] = replay(&scratch, Some(&notebook), &steps)
crates/ui/src/layout.rs+26-9
...@@ -90,13 +90,9 @@ pub(crate) fn solve(...@@ -90,13 +90,9 @@ pub(crate) fn solve(
90 // goes back to the space.90 // goes back to the space.
91 let spare: f32 = space.iter().map(|child| nodes[*child].computed[axis]).sum();91 let spare: f32 = space.iter().map(|child| nodes[*child].computed[axis]).sum();
92 while excess > spare && axis == 0 {92 while excess > spare && axis == 0 {
93 let Some(group) = children93 let mut groups = Vec::new();
94 .iter()94 unfolded(nodes, &children, &mut groups);
95 .copied()95 let Some(group) = groups.into_iter().min_by_key(|group| nodes[*group].fold)
96 .filter(|child| {
97 nodes[*child].fold.is_some() && !nodes[nodes[*child].children[0]].hidden
98 })
99 .min_by_key(|child| nodes[*child].fold)
100 else {96 else {
101 break;97 break;
102 };98 };
...@@ -104,8 +100,14 @@ pub(crate) fn solve(...@@ -104,8 +100,14 @@ pub(crate) fn solve(
104 nodes[full].hidden = true;100 nodes[full].hidden = true;
105 nodes[folded].hidden = false;101 nodes[folded].hidden = false;
106 let width = nodes[folded].computed[0] + 2.0 * nodes[group].pad[0];102 let width = nodes[folded].computed[0] + 2.0 * nodes[group].pad[0];
107 excess -= nodes[group].computed[0] - width;103 let freed = nodes[group].computed[0] - width;
108 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 excess = give_back(nodes, &space, axis, excess);112 excess = give_back(nodes, &space, axis, excess);
111 let mut tiers: Vec<f32> = sized113 let mut tiers: Vec<f32> = sized
...@@ -313,6 +315,21 @@ fn in_flow(nodes: &[Built], index: usize) -> impl Iterator<Item = usize> + '_ {...@@ -313,6 +315,21 @@ fn in_flow(nodes: &[Built], index: usize) -> impl Iterator<Item = usize> + '_ {
313 })315 })
314}316}
315317
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.
320fn 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/// The children's extent on `axis`: summed with gaps along the flow, otherwise the largest.333/// The children's extent on `axis`: summed with gaps along the flow, otherwise the largest.
317fn flow(nodes: &[Built], index: usize, axis: usize) -> f32 {334fn flow(nodes: &[Built], index: usize, axis: usize) -> f32 {
318 let sizes = in_flow(nodes, index).map(|child| nodes[child].computed[axis]);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,7 +390,8 @@ pub struct Spec<'a> {
390 pub anchor: Option<Anchor>,390 pub anchor: Option<Anchor>,
391 /// Makes the box a group of a row in two forms, its first two children, both built every391 /// Makes the box a group of a row in two forms, its first two children, both built every
392 /// frame: its full form, and the one it folds to where the row lacks room. The row folds392 /// 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 has393 /// 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 /// yielded and before any other box gives up room. The form not shown takes no room,395 /// yielded and before any other box gives up room. The form not shown takes no room,
395 /// paint or input, and its boxes take the group's rectangle, so what opens from them396 /// paint or input, and its boxes take the group's rectangle, so what opens from them
396 /// opens from the form shown.397 /// opens from the form shown.
crates/ui/src/shell.rs+44
...@@ -18,6 +18,8 @@ const TAB_ROUNDING: f32 = 4.0;...@@ -18,6 +18,8 @@ const TAB_ROUNDING: f32 = 4.0;
18pub const TOOL: f32 = 22.0;18pub const TOOL: f32 = 22.0;
19/// Width of a menu arrow beside a toolbar button.19/// Width of a menu arrow beside a toolbar button.
20const ARROW: f32 = 10.0;20const ARROW: f32 = 10.0;
21/// Width of a button showing only a menu arrow, which ends its group as tight as an icon does.
22const MORE: f32 = 14.0;
21/// Where a section tab's label starts inside it.23/// Where a section tab's label starts inside it.
22pub const TAB_PAD: f32 = 10.0;24pub const TAB_PAD: f32 = 10.0;
23/// How far section tabs fade out at an end of their row they are cut at, and how near it a25/// 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,6 +485,48 @@ pub fn split_button(
483 ui.signal(button)485 ui.signal(button)
484}486}
485487
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.
490pub 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/// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle530/// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle
487/// lit while `on` as `tool_button`'s. Returns where the menu opens so its icons line up531/// lit while `on` as `tool_button`'s. Returns where the menu opens so its icons line up
488/// under the button's.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,6 +232,79 @@ fn a_row_is_narrowest_with_every_group_folded_and_every_box_squeezed() {
232 assert_eq!(rects[4], Some([40.0, 0.0, 60.0, 20.0]));232 assert_eq!(rects[4], Some([40.0, 0.0, 60.0, 20.0]));
233}233}
234234
235#[test]
236fn 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#[test]308#[test]
236fn a_box_sized_by_its_children_yields_what_they_yield() {309fn a_box_sized_by_its_children_yields_what_they_yield() {
237 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);310 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);