| 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,9 +31,12 @@ keyboard, the toolbar and the macOS menu bar all run commands from it. |
| 31 | ### macOS | 31 | ### macOS |
| 32 | 32 | ||
| 33 | - The toolbar's row is the title bar. An empty `NSToolbar`, unified compact | 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, | 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 drags | 35 | 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 and | 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 | VoiceOver. `--screenshot` paints the lights where the hidden window's AppKit | 40 | 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 the | 41 | put them. The app draws the row as part of the same frame as the rest of the |
| 39 | chrome. Under the whole | 42 | 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. When | 81 | 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 row | 82 | 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`), by | 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 | strict first. That one knob covers most of what flexbox is usually needed | 86 | 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 what | 87 | 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 | } |
| 86 | 86 | ||
| 87 | /// `-mouseDownCanMoveWindow` for the content view: a textured window drags from any press | 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 | /// itself, through `-performWindowDragWithEvent:`. | 90 | /// itself, through `-performWindowDragWithEvent:`. |
| 90 | pub extern "C" fn no_window_drags(_: &AnyObject, _: Sel) -> Bool { | 91 | pub extern "C" fn no_window_drags(_: &AnyObject, _: Sel) -> Bool { |
| 91 | Bool::NO | 92 | 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 gradient | 459 | // 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 | } |
| 1070 | 1070 | ||
| 1071 | /// Zooms the window once the current event is handled: AppKit's zoom animation runs its | 1071 | /// 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 the | 1072 | /// 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. | ||
| 1074 | pub fn zoom(window: &Window) { | 1075 | pub 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.0f64 | 1093 | 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 | }; |
| 136 | 136 | ||
| 137 | /// Height of the toolbar's row, which is the title bar where the platform lets it: a | 137 | /// 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. |
| 139 | const TITLE: f32 = 38.0; | 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 | /// Space between toolbar groups. | 144 | /// Space between toolbar groups. |
| 141 | const GAP: f32 = 6.0; | 145 | const 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 accessibility | 909 | /// 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's | 1554 | // 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::COMMANDS | 2139 | 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 | choice | 2176 | 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 = state | 2228 | let size = state |
| 2234 | .font_size | 2229 | .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 its | 2357 | // 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 | } |
| 2917 | 3009 | ||
| 2918 | /// The search box and the page list's buttons, above the list. Where the tab row lacks | 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,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 | } |
| 5133 | 5227 | ||
| 5134 | /// A toolbar group that folds by `priority`: `full` builds its full form, `folded` the form | 5228 | /// 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. | ||
| 5136 | fn group( | 5231 | fn 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 | ..face | 5244 | ..face |
| 5149 | }, | 5245 | }, |
| ... | @@ -5239,13 +5335,9 @@ fn dropdown( | ... | @@ -5239,13 +5335,9 @@ fn dropdown( |
| 5239 | anchor | 5335 | 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 | anchor | 5356 | 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<_> = entries | 5364 | 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. |
| 19 | const DROP: f32 = 2.0; | 19 | const DROP: f32 = 2.0; |
| 20 | const ROW: f32 = 24.0; | 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 | /// How far each level of the tree sits inside its parent. | 28 | /// How far each level of the tree sits inside its parent. |
| 22 | const INDENT: f32 = 16.0; | 29 | const INDENT: f32 = 16.0; |
| 23 | 30 | ||
| ... | @@ -136,7 +143,8 @@ pub fn header() -> Id { | ... | @@ -136,7 +143,8 @@ pub fn header() -> Id { |
| 136 | 143 | ||
| 137 | /// The sidebar's header row, as tall as the tab row and dragging the window when `drags`, then | 144 | /// 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 button | 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 | fn sidebar( | 148 | fn 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 | } |
| 842 | 854 | ||
| 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 => self | 866 | let edge = self |
| 852 | .ui | 867 | .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 | } |
| 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,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) = children | 93 | 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> = sized | 113 | 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 | } |
| 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 | /// 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. |
| 317 | fn flow(nodes: &[Built], index: usize, axis: usize) -> f32 { | 334 | fn 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 every | 391 | /// 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 folds | 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 | /// 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 them | 396 | /// 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; |
| 18 | pub const TOOL: f32 = 22.0; | 18 | pub 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. |
| 20 | const ARROW: f32 = 10.0; | 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 | /// Where a section tab's label starts inside it. | 23 | /// Where a section tab's label starts inside it. |
| 22 | pub const TAB_PAD: f32 = 10.0; | 24 | pub 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 a | 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,6 +485,48 @@ pub fn split_button( |
| 483 | ui.signal(button) | 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 | /// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle | 530 | /// 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 up | 531 | /// 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 | } |
| 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 | #[test] | 308 | #[test] |
| 236 | fn a_box_sized_by_its_children_yields_what_they_yield() { | 309 | fn 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); |