authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 11:36:49-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 11:56:16-07:00
log93933f26a4f155a3654291ce96aa67bfcf3c69f3
tree0c18d254294cc85f49022aa0bac31d8e7c3f7335
parent1cce9f725367502f129a96c850504b4560cd18c4
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

refactor: popups open beside a box by its id, placed where layout puts it that frame

- `Anchor::Below`, `Right` and `Over` name a box; layout solves the interface first, then each popup beside its box as laid out in the same frame, so a popup opened with its box is never at the window's corner on its first frame - Layout keeps a popup below or over its box at least as wide, opens a submenu past the menu holding its row, and makes a field standing in for a combo as tall as it - `scroll_to` scrolls once the frame is laid out, so it reaches a box built that frame - A section tab's rename field is built inside the tab; the notebook button at the far edge, the locked-section and empty-notebook notices, the recording transport and the templates' close button are placed by layout - A menu button's menu, the conflict bar's menu and the Outlook task's tooltip open from boxes placed where they open - `Ui::rect` gives way to `Ui::laid_out`, the latest layout, for hosts painting the page, mapping its input, and hit-testing drags; `Ui::size` is the window's size this frame Assisted-by: claude-opus-5.5

30 files changed, 815 insertions(+), 591 deletions(-)

crates/snowbound/src/conflict_render.rs+3-3
...@@ -86,7 +86,7 @@ fn paint(...@@ -86,7 +86,7 @@ fn paint(
86 queue: &wgpu::Queue,86 queue: &wgpu::Queue,
87) -> Vec<u8> {87) -> Vec<u8> {
88 let size = SIZE.map(|side| (side * SCALE) as u32);88 let size = SIZE.map(|side| (side * SCALE) as u32);
89 let corner = ui.rect(super::page()).unwrap();89 let corner = ui.laid_out(super::page()).unwrap();
90 let box_size = [90 let box_size = [
91 ((corner[2] - corner[0]) * SCALE) as u32,91 ((corner[2] - corner[0]) * SCALE) as u32,
92 ((corner[3] - corner[1]) * SCALE) as u32,92 ((corner[3] - corner[1]) * SCALE) as u32,
...@@ -281,7 +281,7 @@ fn conflict_views_offer_the_versions_and_render_offscreen() {...@@ -281,7 +281,7 @@ fn conflict_views_offer_the_versions_and_render_offscreen() {
281 other => panic!("{other:?}", other = other.0),281 other => panic!("{other:?}", other = other.0),
282 }282 }
283 let row = Id::ROOT.child("frame").child("panel").child(version);283 let row = Id::ROOT.child("frame").child("panel").child(version);
284 let listed = ui.rect(row).is_some();284 let listed = ui.laid_out(row).is_some();
285 assert_eq!(listed, shown.is_some(), "{name}");285 assert_eq!(listed, shown.is_some(), "{name}");
286 if let (Some(output), Some((renderer, device, queue))) = (&output, &mut gpu) {286 if let (Some(output), Some((renderer, device, queue))) = (&output, &mut gpu) {
287 let shown = session.reader(space)().unwrap();287 let shown = session.reader(space)().unwrap();
...@@ -398,7 +398,7 @@ fn page_versions_list_open_read_only_and_render_offscreen() {...@@ -398,7 +398,7 @@ fn page_versions_list_open_read_only_and_render_offscreen() {
398 }398 }
399 for context in &versions {399 for context in &versions {
400 let row = Id::ROOT.child("frame").child("panel").child(context);400 let row = Id::ROOT.child("frame").child("panel").child(context);
401 assert_eq!(ui.rect(row).is_some(), shown.is_some(), "{name}");401 assert_eq!(ui.laid_out(row).is_some(), shown.is_some(), "{name}");
402 }402 }
403 if let (Some(output), Some((renderer, device, queue))) = (&output, &mut gpu) {403 if let (Some(output), Some((renderer, device, queue))) = (&output, &mut gpu) {
404 let shown = match version {404 let shown = match version {
crates/snowbound/src/live.rs+4-7
...@@ -676,11 +676,8 @@ impl State {...@@ -676,11 +676,8 @@ impl State {
676 ..ui::popup::Item::default()676 ..ui::popup::Item::default()
677 })677 })
678 .collect();678 .collect();
679 let rect = self.ui.rect(self.ui.id("more"));679 let anchor = ui::Anchor::Below(self.ui.id("more"));
680 if let Some(rect) = rect680 if let Some(index) = ui::popup::menu(&mut self.ui, menu, anchor, &items, None) {
681 && let Some(index) =
682 ui::popup::menu(&mut self.ui, menu, ui::Anchor::Below(rect), &items, None)
683 {
684 chosen = Some(peers[index].hello.peer);681 chosen = Some(peers[index].hello.peer);
685 }682 }
686 }683 }
...@@ -874,7 +871,7 @@ impl State {...@@ -874,7 +871,7 @@ impl State {
874 let Some(session) = &self.session else {871 let Some(session) = &self.session else {
875 return;872 return;
876 };873 };
877 let Some([left, top, right, bottom]) = self.ui.rect(page()) else {874 let Some([left, top, right, bottom]) = self.ui.laid_out(page()) else {
878 return;875 return;
879 };876 };
880 let here = (section(session), Some(session.space.into()));877 let here = (section(session), Some(session.space.into()));
...@@ -998,7 +995,7 @@ impl State {...@@ -998,7 +995,7 @@ impl State {
998 [x0, y0, y1]: [f32; 3],995 [x0, y0, y1]: [f32; 3],
999 selection: &[[f32; 4]],996 selection: &[[f32; 4]],
1000 ) {997 ) {
1001 let Some([left, top, ..]) = self.ui.rect(page()) else {998 let Some([left, top, ..]) = self.ui.laid_out(page()) else {
1002 return;999 return;
1003 };1000 };
1004 let id = peer.hello.peer;1001 let id = peer.hello.peer;
crates/snowbound/src/main.rs+93-58
...@@ -1602,7 +1602,7 @@ impl State {...@@ -1602,7 +1602,7 @@ impl State {
1602 |_| [0.0; 4],1602 |_| [0.0; 4],
1603 );1603 );
1604 }1604 }
1605 if let Some(rect) = self.ui.rect(page()) {1605 if let Some(rect) = self.ui.laid_out(page()) {
1606 let size = [1606 let size = [
1607 ((rect[2] - rect[0]) * scale).round() as u32,1607 ((rect[2] - rect[0]) * scale).round() as u32,
1608 ((rect[3] - rect[1]) * scale).round() as u32,1608 ((rect[3] - rect[1]) * scale).round() as u32,
...@@ -1712,7 +1712,7 @@ impl State {...@@ -1712,7 +1712,7 @@ impl State {
1712 .ui1712 .ui
1713 .animate(tab_row(), if self.full_page { 0.0 } else { TAB_ROW });1713 .animate(tab_row(), if self.full_page { 0.0 } else { TAB_ROW });
1714 let drags = self.chrome_drags();1714 let drags = self.chrome_drags();
1715 let tab_row = self.ui.open_as(1715 self.ui.open_as(
1716 tab_row(),1716 tab_row(),
1717 Spec {1717 Spec {
1718 flags: if drags {1718 flags: if drags {
...@@ -1757,24 +1757,41 @@ impl State {...@@ -1757,24 +1757,41 @@ impl State {
1757 let unread = self.unread_keys();1757 let unread = self.unread_keys();
1758 let (clicked, open_tab) = match &self.session {1758 let (clicked, open_tab) = match &self.session {
1759 Some(session) => {1759 Some(session) => {
1760 let lit = self.page_drop(row);
1761 let dragged = self.dragged_tab(row);
1762 let shown = self.open_tab().unwrap_or(session.tab);
1760 // A tab being renamed takes the name typed, which its field covers.1763 // A tab being renamed takes the name typed, which its field covers.
1764 let renaming = self.renaming.as_mut().and_then(|renaming| {
1765 let tab = (session.tabs.iter())
1766 .position(|tab| renaming.entry(&session.library, &tab.path, true))?;
1767 Some((tab, &mut renaming.name))
1768 });
1769 let typed = renaming
1770 .as_ref()
1771 .map(|(tab, name)| (*tab, name.to_string()));
1761 let tabs: Vec<_> = session1772 let tabs: Vec<_> = session
1762 .tabs1773 .tabs
1763 .iter()1774 .iter()
1764 .map(|tab| {1775 .enumerate()
1765 let name = match &self.renaming {1776 .map(|(index, tab)| {
1766 Some(renaming) if renaming.entry(&session.library, &tab.path, true) => {1777 let name = match &typed {
1767 renaming.name.as_str()1778 Some((renamed, name)) if *renamed == index => name.as_str(),
1768 }
1769 _ => tab.name.as_str(),1779 _ => tab.name.as_str(),
1770 };1780 };
1771 let unread = unread.contains(&session.library.key(&tab.path));1781 let unread = unread.contains(&session.library.key(&tab.path));
1772 (name, section_color(tab.color), unread)1782 (name, section_color(tab.color), unread)
1773 })1783 })
1774 .collect();1784 .collect();
1775 let lit = self.page_drop(row);1785 let mut kept = None;
1776 let dragged = self.dragged_tab(row);1786 let mut field;
1777 let shown = self.open_tab().unwrap_or(session.tab);1787 let renaming = match renaming {
1788 Some((tab, name)) => {
1789 field =
1790 |ui: &mut Ui, tall| kept = rename::tab_field(ui, &theme, name, tall);
1791 Some((tab, &mut field as &mut dyn FnMut(&mut Ui, f32)))
1792 }
1793 None => None,
1794 };
1778 let ui::shell::Tabs {1795 let ui::shell::Tabs {
1779 clicked,1796 clicked,
1780 context,1797 context,
...@@ -1790,10 +1807,14 @@ impl State {...@@ -1790,10 +1807,14 @@ impl State {
1790 shown,1807 shown,
1791 lit,1808 lit,
1792 dragged,1809 dragged,
1810 renaming,
1793 &section,1811 &section,
1794 TAB_ROW,1812 TAB_ROW,
1795 theme.strip,1813 theme.strip,
1796 );1814 );
1815 if let Some(keep) = kept {
1816 self.finish_renaming(keep);
1817 }
1797 name(&mut self.ui, row, "Sections");1818 name(&mut self.ui, row, "Sections");
1798 self.drag_tabs(held, slot, settled, row);1819 self.drag_tabs(held, slot, settled, row);
1799 let Some(session) = &self.session else {1820 let Some(session) = &self.session else {
...@@ -1825,7 +1846,6 @@ impl State {...@@ -1825,7 +1846,6 @@ impl State {
1825 };1846 };
1826 self.rename(target);1847 self.rename(target);
1827 }1848 }
1828 self.tab_rename_field(&theme, row, tab_row);
1829 (clicked, open_tab)1849 (clicked, open_tab)
1830 }1850 }
1831 None if self.locked.is_some() => self.locked_tabs(row, &section, theme.strip),1851 None if self.locked.is_some() => self.locked_tabs(row, &section, theme.strip),
...@@ -1839,6 +1859,7 @@ impl State {...@@ -1839,6 +1859,7 @@ impl State {
1839 0,1859 0,
1840 None,1860 None,
1841 None,1861 None,
1862 None,
1842 &section,1863 &section,
1843 TAB_ROW,1864 TAB_ROW,
1844 theme.strip,1865 theme.strip,
...@@ -1963,9 +1984,7 @@ impl State {...@@ -1963,9 +1984,7 @@ impl State {
1963 if !opening {1984 if !opening {
1964 self.template_strip(&theme);1985 self.template_strip(&theme);
1965 }1986 }
1966 if let Some(rect) = self.ui.rect(page()) {1987 self.transport(&theme)?;
1967 self.transport(&theme, rect)?;
1968 }
1969 let scroll = self.view.scroll();1988 let scroll = self.view.scroll();
1970 for (index, axis) in [Axis::X, Axis::Y]1989 for (index, axis) in [Axis::X, Axis::Y]
1971 .into_iter()1990 .into_iter()
...@@ -1990,20 +2009,24 @@ impl State {...@@ -1990,20 +2009,24 @@ impl State {
1990 if !opening {2009 if !opening {
1991 self.peer_carets();2010 self.peer_carets();
1992 }2011 }
1993 self.ui.close();2012 let task = self.view.task_under_pointer().map(|[x0, y0, x1, y1]| {
1994 if let Some(task) = self.view.task_under_pointer() {
1995 let [left, top, ..] = self.ui.rect(page()).unwrap_or_default();
1996 let scale = self.ui.scale();2013 let scale = self.ui.scale();
1997 let [x0, y0, x1, y1] = task;2014 self.ui.leaf(
1998 let part = [2015 "task",
1999 left + x0 / scale,2016 Spec {
2000 top + y0 / scale,2017 flags: Flags::FLOAT,
2001 left + x1 / scale,2018 size: [px((x1 - x0) / scale), px((y1 - y0) / scale)],
2002 top + y1 / scale,2019 position: [x0 / scale, y0 / scale],
2003 ];2020 ..Spec::default()
2021 },
2022 );
2023 page().child("task")
2024 });
2025 self.ui.close();
2026 if let Some(task) = task {
2004 ui::popup::tooltip_over(2027 ui::popup::tooltip_over(
2005 &mut self.ui,2028 &mut self.ui,
2006 part,2029 task,
2007 "Outlook task",2030 "Outlook task",
2008 Some("Edit it in OneNote with Outlook"),2031 Some("Edit it in OneNote with Outlook"),
2009 );2032 );
...@@ -2046,8 +2069,9 @@ impl State {...@@ -2046,8 +2069,9 @@ impl State {
2046 /// Borders the open section tab and the frame's top, and rounds and borders the page2069 /// Borders the open section tab and the frame's top, and rounds and borders the page
2047 /// together with the open page's tab, joined where they meet.2070 /// together with the open page's tab, joined where they meet.
2048 fn edges(&mut self, section: ui::Section, open_tab: Id, open_page: Option<Id>) {2071 fn edges(&mut self, section: ui::Section, open_tab: Id, open_page: Option<Id>) {
2049 let panel = self.ui.rect(frame().child("panel"));2072 let panel = self.ui.laid_out(frame().child("panel"));
2050 let (Some(frame), Some(page)) = (self.ui.rect(frame()), self.ui.rect(page())) else {2073 let (Some(frame), Some(page)) = (self.ui.laid_out(frame()), self.ui.laid_out(page()))
2074 else {
2051 return;2075 return;
2052 };2076 };
2053 let rounding = self.rounding();2077 let rounding = self.rounding();
...@@ -2056,7 +2080,7 @@ impl State {...@@ -2056,7 +2080,7 @@ impl State {
2056 let [left, top, right, bottom] = page;2080 let [left, top, right, bottom] = page;
2057 let left_tabs = self.page_tabs_left;2081 let left_tabs = self.page_tabs_left;
2058 let tab = open_page2082 let tab = open_page
2059 .and_then(|id| self.ui.rect(id))2083 .and_then(|id| self.ui.laid_out(id))
2060 .map(|row| [row[0], row[1], row[2], row[3] - ROW_GAP])2084 .map(|row| [row[0], row[1], row[2], row[3] - ROW_GAP])
2061 .zip(panel)2085 .zip(panel)
2062 .and_then(|(row, panel)| {2086 .and_then(|(row, panel)| {
...@@ -2129,7 +2153,7 @@ impl State {...@@ -2129,7 +2153,7 @@ impl State {
2129 // the open tab stands on it.2153 // the open tab stands on it.
2130 let [start, end] = [frame[0], frame[2]];2154 let [start, end] = [frame[0], frame[2]];
2131 let outer = platform::corner_radius(&self.window);2155 let outer = platform::corner_radius(&self.window);
2132 let window = self.ui.rect(Id::ROOT).map_or(end, |root| root[2]);2156 let window = self.ui.size()[0];
2133 let [beside, beside_right] =2157 let [beside, beside_right] =
2134 [start, window - end].map(|gap| (outer * gap / sidebar::WIDTH).min(outer));2158 [start, window - end].map(|gap| (outer * gap / sidebar::WIDTH).min(outer));
2135 let strip = self.ui.theme.strip;2159 let strip = self.ui.theme.strip;
...@@ -2153,11 +2177,11 @@ impl State {...@@ -2153,11 +2177,11 @@ impl State {
2153 // The border breaks where the open tab shows, which the row may scroll it out of.2177 // The border breaks where the open tab shows, which the row may scroll it out of.
2154 let [left, right] = self2178 let [left, right] = self
2155 .ui2179 .ui
2156 .rect(sections())2180 .laid_out(sections())
2157 .map_or([end; 2], |row| [row[0], row[2]]);2181 .map_or([end; 2], |row| [row[0], row[2]]);
2158 let [foot, toe] = self2182 let [foot, toe] = self
2159 .ui2183 .ui
2160 .rect(open_tab)2184 .laid_out(open_tab)
2161 .map_or([end; 2], |tab| ui::shell::tab_base(tab, TAB_ROW))2185 .map_or([end; 2], |tab| ui::shell::tab_base(tab, TAB_ROW))
2162 .map(|x| x.clamp(left, right));2186 .map(|x| x.clamp(left, right));
2163 // The border runs down both sides to the window's bottom, round the bottom corner2187 // The border runs down both sides to the window's bottom, round the bottom corner
...@@ -2398,7 +2422,7 @@ impl State {...@@ -2398,7 +2422,7 @@ impl State {
2398 disabled: !enabled,2422 disabled: !enabled,
2399 ..Default::default()2423 ..Default::default()
2400 }));2424 }));
2401 let anchor = ui::Anchor::Below(ui.rect(ui.id("menu")).unwrap_or_default());2425 let anchor = ui::Anchor::Below(ui.id("menu"));
2402 if ui::popup::menu(ui, toolbar_popup("paste"), anchor, &items, None).is_some() {2426 if ui::popup::menu(ui, toolbar_popup("paste"), anchor, &items, None).is_some() {
2403 choice = Some(Choice::Command(Cmd::Paste));2427 choice = Some(Choice::Command(Cmd::Paste));
2404 }2428 }
...@@ -2422,7 +2446,7 @@ impl State {...@@ -2422,7 +2446,7 @@ impl State {
2422 ui::shell::menu_button(ui, "styles", art::STYLES, None, styles)2446 ui::shell::menu_button(ui, "styles", art::STYLES, None, styles)
2423 } else {2447 } else {
2424 ui::shell::unavailable(ui, "styles", art::STYLES, text, true);2448 ui::shell::unavailable(ui, "styles", art::STYLES, text, true);
2425 ui::Anchor::Below(ui.rect(ui.id("styles")).unwrap_or_default())2449 ui::Anchor::Below(ui.id("styles"))
2426 };2450 };
2427 let shown = notebook::sidecar::themes::STYLES2451 let shown = notebook::sidecar::themes::STYLES
2428 .iter()2452 .iter()
...@@ -2485,7 +2509,7 @@ impl State {...@@ -2485,7 +2509,7 @@ impl State {
2485 ..Default::default()2509 ..Default::default()
2486 })2510 })
2487 .collect();2511 .collect();
2488 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());2512 let anchor = ui::Anchor::Over(combo);
2489 if let Some(index) =2513 if let Some(index) =
2490 ui::popup::menu(ui, toolbar_popup("font"), anchor, &items, Some("Font"))2514 ui::popup::menu(ui, toolbar_popup("font"), anchor, &items, Some("Font"))
2491 && let Some(name) = choices[index].12515 && let Some(name) = choices[index].1
...@@ -2534,7 +2558,7 @@ impl State {...@@ -2534,7 +2558,7 @@ impl State {
2534 ..Default::default()2558 ..Default::default()
2535 });2559 });
2536 }2560 }
2537 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());2561 let anchor = ui::Anchor::Over(combo);
2538 if let Some(index) = ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size))2562 if let Some(index) = ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size))
2539 {2563 {
2540 choice = Some(Choice::Size(sizes[index]));2564 choice = Some(Choice::Size(sizes[index]));
...@@ -2601,7 +2625,7 @@ impl State {...@@ -2601,7 +2625,7 @@ impl State {
2601 .iter()2625 .iter()
2602 .map(|&(color, name)| (colorref(color), name))2626 .map(|&(color, name)| (colorref(color), name))
2603 .collect();2627 .collect();
2604 let anchor = ui::Anchor::Below(ui.rect(split).unwrap_or_default());2628 let anchor = ui::Anchor::Below(split);
2605 if let Some(chosen) =2629 if let Some(chosen) =
2606 ui::popup::colors(ui, toolbar_popup(part), anchor, none, &colors, columns)2630 ui::popup::colors(ui, toolbar_popup(part), anchor, none, &colors, columns)
2607 {2631 {
...@@ -2681,8 +2705,7 @@ impl State {...@@ -2681,8 +2705,7 @@ impl State {
2681 }2705 }
2682 tip(ui, id);2706 tip(ui, id);
2683 }2707 }
2684 let [bullets, numbering] =2708 let [bullets, numbering] = ["bullets", "numbering"].map(|part| ui.id(part));
2685 ["bullets", "numbering"].map(|part| ui.rect(ui.id(part)).unwrap_or_default());
2686 let current = |bullet| match state.list {2709 let current = |bullet| match state.list {
2687 Some(ListStyle::Bullet(place)) if bullet => Some(place),2710 Some(ListStyle::Bullet(place)) if bullet => Some(place),
2688 Some(ListStyle::Number(place)) if !bullet => Some(place),2711 Some(ListStyle::Number(place)) if !bullet => Some(place),
...@@ -2982,7 +3005,7 @@ impl State {...@@ -2982,7 +3005,7 @@ impl State {
2982 choice = Some(Choice::Command(Cmd::Pen));3005 choice = Some(Choice::Command(Cmd::Pen));
2983 }3006 }
2984 tip(ui, Cmd::Pen);3007 tip(ui, Cmd::Pen);
2985 let anchor = ui::Anchor::Below(ui.rect(split).unwrap_or_default());3008 let anchor = ui::Anchor::Below(split);
2986 if let Some(place) = pen_gallery(ui, anchor, &drawing_pens, pens.pen) {3009 if let Some(place) = pen_gallery(ui, anchor, &drawing_pens, pens.pen) {
2987 choice = Some(Choice::Pen(place));3010 choice = Some(Choice::Pen(place));
2988 }3011 }
...@@ -3367,7 +3390,7 @@ impl State {...@@ -3367,7 +3390,7 @@ impl State {
3367 }3390 }
3368 let read_only = self.session.as_ref().is_some_and(Session::read_only);3391 let read_only = self.session.as_ref().is_some_and(Session::read_only);
3369 let scale = self.ui.scale();3392 let scale = self.ui.scale();
3370 let corner = self.ui.rect(page()).unwrap_or_default();3393 let corner = self.ui.laid_out(page()).unwrap_or_default();
3371 let device = |point: [f32; 2]| {3394 let device = |point: [f32; 2]| {
3372 [3395 [
3373 (point[0] - corner[0]) * scale,3396 (point[0] - corner[0]) * scale,
...@@ -3934,7 +3957,7 @@ impl State {...@@ -3934,7 +3957,7 @@ impl State {
3934 /// Follows the view moving: the input method's position and accessibility.3957 /// Follows the view moving: the input method's position and accessibility.
3935 fn after_move(&mut self) -> Result<(), Box<dyn Error>> {3958 fn after_move(&mut self) -> Result<(), Box<dyn Error>> {
3936 let scale = self.ui.scale();3959 let scale = self.ui.scale();
3937 let corner = self.ui.rect(page()).unwrap_or_default();3960 let corner = self.ui.laid_out(page()).unwrap_or_default();
3938 let [x0, y0, x1, y1] = self.view.caret_area()?;3961 let [x0, y0, x1, y1] = self.view.caret_area()?;
3939 self.window.set_ime_cursor_area(3962 self.window.set_ime_cursor_area(
3940 LogicalPosition::new(x0 / scale + corner[0], y0 / scale + corner[1]),3963 LogicalPosition::new(x0 / scale + corner[0], y0 / scale + corner[1]),
...@@ -4292,7 +4315,7 @@ impl State {...@@ -4292,7 +4315,7 @@ impl State {
4292 /// `page` asks or its tree is not grafted yet. The interface's tree goes first, since it4315 /// `page` asks or its tree is not grafted yet. The interface's tree goes first, since it
4293 /// holds the page's.4316 /// holds the page's.
4294 fn update_accessibility(&mut self, page: bool) -> Result<(), Box<dyn Error>> {4317 fn update_accessibility(&mut self, page: bool) -> Result<(), Box<dyn Error>> {
4295 let built = self.ui.rect(self::page()).is_some();4318 let built = self.ui.laid_out(self::page()).is_some();
4296 if !built && self.page_grafted {4319 if !built && self.page_grafted {
4297 // Its graft is gone, and with it the tree, which must be sent whole again.4320 // Its graft is gone, and with it the tree, which must be sent whole again.
4298 self.accessibility.deactivate();4321 self.accessibility.deactivate();
...@@ -4396,12 +4419,12 @@ impl State {...@@ -4396,12 +4419,12 @@ impl State {
4396 .accessibility_tree(&self.window.title(), self.window.scale_factor());4419 .accessibility_tree(&self.window.title(), self.window.scale_factor());
4397 let focus = std::mem::replace(&mut chrome.focus, ui::Id::ROOT.node());4420 let focus = std::mem::replace(&mut chrome.focus, ui::Id::ROOT.node());
4398 let mut tree = Tree::new(chrome, true);4421 let mut tree = Tree::new(chrome, true);
4399 if self.ui.rect(page()).is_some() {4422 if self.ui.laid_out(page()).is_some() {
4400 let mut fresh = accessibility::Accessibility::default();4423 let mut fresh = accessibility::Accessibility::default();
4401 let page = page_tree(&self.view, &self.ui, &self.window, &mut fresh)?;4424 let page = page_tree(&self.view, &self.ui, &self.window, &mut fresh)?;
4402 tree.update_and_process_changes(page, &mut Unwatched);4425 tree.update_and_process_changes(page, &mut Unwatched);
4403 }4426 }
4404 if focus != graft || self.ui.rect(page()).is_some() {4427 if focus != graft || self.ui.laid_out(page()).is_some() {
4405 let update = accesskit::TreeUpdate {4428 let update = accesskit::TreeUpdate {
4406 nodes: Vec::new(),4429 nodes: Vec::new(),
4407 tree: None,4430 tree: None,
...@@ -4567,7 +4590,7 @@ impl State {...@@ -4567,7 +4590,7 @@ impl State {
4567 }4590 }
4568 let theme = &self.ui.theme;4591 let theme = &self.ui.theme;
4569 let scale = self.ui.scale();4592 let scale = self.ui.scale();
4570 let corner = self.ui.rect(page()).unwrap_or_default();4593 let corner = self.ui.laid_out(page()).unwrap_or_default();
4571 // A page taking a while to open shows its outline, drawn in points from the corner.4594 // A page taking a while to open shows its outline, drawn in points from the corner.
4572 let (page_primitives, viewport) = match self.loading() {4595 let (page_primitives, viewport) = match self.loading() {
4573 Some(since) => (4596 Some(since) => (
...@@ -4640,14 +4663,14 @@ impl State {...@@ -4640,14 +4663,14 @@ impl State {
4640 }4663 }
46414664
4642 fn over_page(&self) -> bool {4665 fn over_page(&self) -> bool {
4643 let [x0, y0, x1, y1] = self.ui.rect(page()).unwrap_or_default();4666 let [x0, y0, x1, y1] = self.ui.laid_out(page()).unwrap_or_default();
4644 let [x, y] = self.pointer;4667 let [x, y] = self.pointer;
4645 (x0..x1).contains(&x) && (y0..y1).contains(&y)4668 (x0..x1).contains(&x) && (y0..y1).contains(&y)
4646 }4669 }
46474670
4648 /// Window point `point` in the page's device pixels.4671 /// Window point `point` in the page's device pixels.
4649 fn page_point(&self, point: [f32; 2]) -> [f32; 2] {4672 fn page_point(&self, point: [f32; 2]) -> [f32; 2] {
4650 let [left, top, ..] = self.ui.rect(page()).unwrap_or_default();4673 let [left, top, ..] = self.ui.laid_out(page()).unwrap_or_default();
4651 let scale = self.ui.scale();4674 let scale = self.ui.scale();
4652 [(point[0] - left) * scale, (point[1] - top) * scale]4675 [(point[0] - left) * scale, (point[1] - top) * scale]
4653 }4676 }
...@@ -4777,7 +4800,7 @@ fn page_tree(...@@ -4777,7 +4800,7 @@ fn page_tree(
4777 window: &Window,4800 window: &Window,
4778 access: &mut accessibility::Accessibility,4801 access: &mut accessibility::Accessibility,
4779) -> Result<accesskit::TreeUpdate, onestore::page::text::EditError> {4802) -> Result<accesskit::TreeUpdate, onestore::page::text::EditError> {
4780 let corner = ui.rect(page()).unwrap_or_default();4803 let corner = ui.laid_out(page()).unwrap_or_default();
4781 let scale = ui.scale();4804 let scale = ui.scale();
4782 let ratio = window.scale_factor() as f32 / scale;4805 let ratio = window.scale_factor() as f32 / scale;
4783 let viewport = canvas::gpu::Viewport {4806 let viewport = canvas::gpu::Viewport {
...@@ -4868,8 +4891,8 @@ fn conflict_bar(ui: &mut Ui, bar: Bar, sections: &[&str], steps: [bool; 2]) -> O...@@ -4868,8 +4891,8 @@ fn conflict_bar(ui: &mut Ui, bar: Bar, sections: &[&str], steps: [bool; 2]) -> O
4868 ..Spec::default()4891 ..Spec::default()
4869 },4892 },
4870 );4893 );
4871 for (part, text) in [("said", said), ("action", action)] {4894 let line = |ui: &mut Ui, part: &str, text| {
4872 ui.leaf(4895 ui.open(
4873 part,4896 part,
4874 Spec {4897 Spec {
4875 size: [fill(), fit()],4898 size: [fill(), fit()],
...@@ -4879,16 +4902,28 @@ fn conflict_bar(ui: &mut Ui, bar: Bar, sections: &[&str], steps: [bool; 2]) -> O...@@ -4879,16 +4902,28 @@ fn conflict_bar(ui: &mut Ui, bar: Bar, sections: &[&str], steps: [bool; 2]) -> O
4879 ..Spec::default()4902 ..Spec::default()
4880 },4903 },
4881 );4904 );
4882 }4905 };
4883 let action = ui.id("action");4906 line(ui, "said", said);
4907 ui.close();
4908 line(ui, "action", action);
4909 // The menu opens under the line the click asks for, from its leading edge, as wide as
4910 // its items.
4911 let from = ui.id("menu");
4912 ui.leaf(
4913 "menu",
4914 Spec {
4915 flags: Flags::FLOAT,
4916 size: [px(0.0), fill()],
4917 ..Spec::default()
4918 },
4919 );
4920 ui.close();
4884 ui.close();4921 ui.close();
4885 ui.close();4922 ui.close();
4886 let clicked = ui.signal(row).clicked;4923 let clicked = ui.signal(row).clicked;
4887 let menu = Id::ROOT.child("conflict-menu");4924 let menu = Id::ROOT.child("conflict-menu");
4888 let copy = Id::ROOT.child("conflict-copy");4925 let copy = Id::ROOT.child("conflict-copy");
4889 // The menu opens under the line the click asks for, as wide as its items.4926 let anchor = ui::Anchor::Below(from);
4890 let [left, top, _, bottom] = ui.rect(action).unwrap_or_default();
4891 let anchor = ui::Anchor::Below([left, top, left, bottom]);
4892 match bar {4927 match bar {
4893 Bar::Page { page, shown } => clicked.then_some(Command::Versions { page, show: !shown }),4928 Bar::Page { page, shown } => clicked.then_some(Command::Versions { page, show: !shown }),
4894 Bar::History {4929 Bar::History {
...@@ -5568,7 +5603,7 @@ fn dropdown(...@@ -5568,7 +5603,7 @@ fn dropdown(
5568 choice = Some(Choice::Command(id));5603 choice = Some(Choice::Command(id));
5569 }5604 }
5570 tip(ui, id);5605 tip(ui, id);
5571 ui::Anchor::Below(ui.rect(button).unwrap_or_default())5606 ui::Anchor::Below(button)
5572 }5607 }
5573 Head::Tag(place, tag) if status_of(commands::Id::Tag(place)).enabled => {5608 Head::Tag(place, tag) if status_of(commands::Id::Tag(place)).enabled => {
5574 let on = status_of(commands::Id::Tag(place)).checked;5609 let on = status_of(commands::Id::Tag(place)).checked;
...@@ -5577,7 +5612,7 @@ fn dropdown(...@@ -5577,7 +5612,7 @@ fn dropdown(
5577 choice = Some(Choice::Command(commands::Id::Tag(place)));5612 choice = Some(Choice::Command(commands::Id::Tag(place)));
5578 }5613 }
5579 tag_tip(ui, place, tag);5614 tag_tip(ui, place, tag);
5580 ui::Anchor::Below(ui.rect(button).unwrap_or_default())5615 ui::Anchor::Below(button)
5581 }5616 }
5582 // Where its command does not apply, the button only opens the menu.5617 // Where its command does not apply, the button only opens the menu.
5583 Head::Split(id) => {5618 Head::Split(id) => {
...@@ -5596,7 +5631,7 @@ fn dropdown(...@@ -5596,7 +5631,7 @@ fn dropdown(
5596 let tint = ui.theme.text;5631 let tint = ui.theme.text;
5597 ui::shell::unavailable(ui, part, icon, tint, true);5632 ui::shell::unavailable(ui, part, icon, tint, true);
5598 name(ui, button, label);5633 name(ui, button, label);
5599 ui::Anchor::Below(ui.rect(button).unwrap_or_default())5634 ui::Anchor::Below(button)
5600 }5635 }
5601 Head::Menu(label, icon) => {5636 Head::Menu(label, icon) => {
5602 let anchor = ui::shell::menu_button(ui, part, icon, None, menu);5637 let anchor = ui::shell::menu_button(ui, part, icon, None, menu);
crates/snowbound/src/menus.rs+8-8
...@@ -406,7 +406,7 @@ impl State {...@@ -406,7 +406,7 @@ impl State {
406 shown406 shown
407 && self407 && self
408 .ui408 .ui
409 .rect(crate::sections())409 .laid_out(crate::sections())
410 .is_some_and(|[left, top, right, bottom]| {410 .is_some_and(|[left, top, right, bottom]| {
411 x >= left && x < right && y >= top && y < bottom411 x >= left && x < right && y >= top && y < bottom
412 })412 })
...@@ -1108,7 +1108,7 @@ impl State {...@@ -1108,7 +1108,7 @@ impl State {
1108 (0..session.tabs.len())1108 (0..session.tabs.len())
1109 .filter(|tab| *tab != session.tab)1109 .filter(|tab| *tab != session.tab)
1110 .find(|tab| {1110 .find(|tab| {
1111 self.ui.rect(ui::shell::tab_id(row, *tab)).is_some_and(1111 self.ui.laid_out(ui::shell::tab_id(row, *tab)).is_some_and(
1112 |[left, top, right, bottom]| x >= left && x < right && y >= top && y < bottom,1112 |[left, top, right, bottom]| x >= left && x < right && y >= top && y < bottom,
1113 )1113 )
1114 })1114 })
...@@ -1120,7 +1120,7 @@ impl State {...@@ -1120,7 +1120,7 @@ impl State {
1120 let Dragged::Page(space) = drag.what else {1120 let Dragged::Page(space) = drag.what else {
1121 return None;1121 return None;
1122 };1122 };
1123 let origin = self.ui.rect(self.ui.id(crate::page_list_top()))?[1];1123 let origin = self.ui.laid_out(self.ui.id(crate::page_list_top()))?[1];
1124 Some(crate::PageDrag {1124 Some(crate::PageDrag {
1125 space,1125 space,
1126 top: drag.live().then(|| drag.corner(self.pointer)[1] - origin),1126 top: drag.live().then(|| drag.corner(self.pointer)[1] - origin),
...@@ -1142,10 +1142,10 @@ impl State {...@@ -1142,10 +1142,10 @@ impl State {
1142 ) {1142 ) {
1143 let mine = |what: &Dragged| matches!(what, Dragged::Page(_));1143 let mine = |what: &Dragged| matches!(what, Dragged::Page(_));
1144 self.settle(mine, settled);1144 self.settle(mine, settled);
1145 let held =1145 let held = held
1146 held.and_then(|space| Some((Dragged::Page(space), self.ui.rect(self.ui.id(space))?)));1146 .and_then(|space| Some((Dragged::Page(space), self.ui.laid_out(self.ui.id(space))?)));
1147 let target = self.page_drop(row);1147 let target = self.page_drop(row);
1148 let origin = self.ui.rect(self.ui.id(crate::page_list_top()));1148 let origin = self.ui.laid_out(self.ui.id(crate::page_list_top()));
1149 let dropped = self.follow_drag(held, mine);1149 let dropped = self.follow_drag(held, mine);
1150 let (1150 let (
1151 Some(session),1151 Some(session),
...@@ -1217,7 +1217,7 @@ impl State {...@@ -1217,7 +1217,7 @@ impl State {
1217 let Dragged::Tab(index) = drag.what else {1217 let Dragged::Tab(index) = drag.what else {
1218 return None;1218 return None;
1219 };1219 };
1220 let start = drag.corner(self.pointer)[0] - self.ui.rect(row)?[0];1220 let start = drag.corner(self.pointer)[0] - self.ui.laid_out(row)?[0];
1221 Some(ui::shell::Dragged {1221 Some(ui::shell::Dragged {
1222 index,1222 index,
1223 start: drag.live().then_some(start),1223 start: drag.live().then_some(start),
...@@ -1239,7 +1239,7 @@ impl State {...@@ -1239,7 +1239,7 @@ impl State {
1239 let held = held.and_then(|tab| {1239 let held = held.and_then(|tab| {
1240 Some((1240 Some((
1241 Dragged::Tab(tab),1241 Dragged::Tab(tab),
1242 self.ui.rect(ui::shell::tab_id(row, tab))?,1242 self.ui.laid_out(ui::shell::tab_id(row, tab))?,
1243 ))1243 ))
1244 });1244 });
1245 let dropped = self.follow_drag(held, mine);1245 let dropped = self.follow_drag(held, mine);
crates/snowbound/src/options.rs+6-6
...@@ -521,8 +521,8 @@ impl State {...@@ -521,8 +521,8 @@ impl State {
521 // The section in the top quarter of the list, or the last once the list is scrolled to521 // The section in the top quarter of the list, or the last once the list is scrolled to
522 // its end.522 // its end.
523 let current = options.picked.or_else(|| {523 let current = options.picked.or_else(|| {
524 let [_, top, _, bottom] = ui.rect(list())?;524 let [_, top, _, bottom] = ui.laid_out(list())?;
525 let starts = |id| ui.rect(id).map(|rect: [f32; 4]| rect[1]);525 let starts = |id| ui.laid_out(id).map(|rect: [f32; 4]| rect[1]);
526 let end = starts(list().child("top")).is_some_and(|start| start < top - 1.0)526 let end = starts(list().child("top")).is_some_and(|start| start < top - 1.0)
527 && starts(list().child("end")).is_some_and(|end| end <= bottom + 1.0);527 && starts(list().child("end")).is_some_and(|end| end <= bottom + 1.0);
528 sections528 sections
...@@ -886,7 +886,7 @@ fn dropdown(ui: &mut Ui, name: &str, choices: &[&str], chosen: usize, width: f32...@@ -886,7 +886,7 @@ fn dropdown(ui: &mut Ui, name: &str, choices: &[&str], chosen: usize, width: f32
886 ..Item::default()886 ..Item::default()
887 })887 })
888 .collect();888 .collect();
889 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());889 let anchor = Anchor::Below(combo);
890 ui::popup::menu(ui, menu, anchor, &items, None)890 ui::popup::menu(ui, menu, anchor, &items, None)
891}891}
892892
...@@ -968,7 +968,7 @@ fn renderer(state: &mut State, options: &mut Options, name: &str) {...@@ -968,7 +968,7 @@ fn renderer(state: &mut State, options: &mut Options, name: &str) {
968 ..Item::default()968 ..Item::default()
969 })969 })
970 .collect();970 .collect();
971 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());971 let anchor = Anchor::Below(combo);
972 if let Some(index) = ui::popup::menu(ui, menu, anchor, &items, None) {972 if let Some(index) = ui::popup::menu(ui, menu, anchor, &items, None) {
973 let picked = Backend::PLATFORM[index];973 let picked = Backend::PLATFORM[index];
974 options.renderer = if Some(picked) == automatic {974 options.renderer = if Some(picked) == automatic {
...@@ -1044,7 +1044,7 @@ fn font_face(state: &mut State, options: &mut Options, name: &str) {...@@ -1044,7 +1044,7 @@ fn font_face(state: &mut State, options: &mut Options, name: &str) {
1044 ..Item::default()1044 ..Item::default()
1045 })1045 })
1046 .collect();1046 .collect();
1047 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());1047 let anchor = Anchor::Over(combo);
1048 if let Some(index) = ui::popup::menu(ui, self::fonts(), anchor, &items, Some(name)) {1048 if let Some(index) = ui::popup::menu(ui, self::fonts(), anchor, &items, Some(name)) {
1049 font.face = fonts[index].clone();1049 font.face = fonts[index].clone();
1050 }1050 }
...@@ -1083,7 +1083,7 @@ fn font_color(state: &mut State, options: &mut Options, name: &str) {...@@ -1083,7 +1083,7 @@ fn font_color(state: &mut State, options: &mut Options, name: &str) {
1083 .iter()1083 .iter()
1084 .map(|&(color, name)| (canvas::gpu::colorref(color), name))1084 .map(|&(color, name)| (canvas::gpu::colorref(color), name))
1085 .collect();1085 .collect();
1086 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());1086 let anchor = Anchor::Below(combo);
1087 if let Some(chosen) = ui::popup::colors(ui, font_colors(), anchor, "Automatic", &swatches, 10) {1087 if let Some(chosen) = ui::popup::colors(ui, font_colors(), anchor, "Automatic", &swatches, 10) {
1088 font.color = chosen.and_then(|chosen| {1088 font.color = chosen.and_then(|chosen| {
1089 let at = swatches.iter().position(|(swatch, _)| *swatch == chosen)?;1089 let at = swatches.iter().position(|(swatch, _)| *swatch == chosen)?;
crates/snowbound/src/palette.rs+2-2
...@@ -224,7 +224,7 @@ impl State {...@@ -224,7 +224,7 @@ impl State {
224 ui::popup::submenus(&mut self.ui, id(), &items[0], |index| {224 ui::popup::submenus(&mut self.ui, id(), &items[0], |index| {
225 commands[index].submenu.then(|| submenu(index))225 commands[index].submenu.then(|| submenu(index))
226 });226 });
227 let anchor = Anchor::Right(self.ui.rect(id()).unwrap_or_default());227 let anchor = Anchor::Right(id());
228 for (index, row) in commands.iter().enumerate() {228 for (index, row) in commands.iter().enumerate() {
229 if let (Some(target), Some(action)) = (&row.target, &row.action)229 if let (Some(target), Some(action)) = (&row.target, &row.action)
230 && let Some(chosen) = self.submenu(submenu(index), action, target, anchor)230 && let Some(chosen) = self.submenu(submenu(index), action, target, anchor)
...@@ -307,7 +307,7 @@ impl State {...@@ -307,7 +307,7 @@ impl State {
307 (action, Item { separated, ..item })307 (action, Item { separated, ..item })
308 },308 },
309 ));309 ));
310 let anchor = Anchor::Right(self.ui.rect(id()).unwrap_or_default());310 let anchor = Anchor::Right(id());
311 if let Some(action) =311 if let Some(action) =
312 self.action_menu(panel, anchor, Some("Search actions"), &target, &actions)312 self.action_menu(panel, anchor, Some("Search actions"), &target, &actions)
313 {313 {
crates/snowbound/src/pane.rs+1-1
...@@ -360,7 +360,7 @@ fn choice(...@@ -360,7 +360,7 @@ fn choice(
360 ..Default::default()360 ..Default::default()
361 })361 })
362 .collect();362 .collect();
363 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());363 let anchor = Anchor::Below(combo);
364 ui::popup::menu(ui, popup(name), anchor, &items, None)364 ui::popup::menu(ui, popup(name), anchor, &items, None)
365}365}
366366
crates/snowbound/src/print.rs+1-1
...@@ -99,7 +99,7 @@ fn choose(ui: &mut Ui, name: &str, names: &[&str], current: usize) -> Option<usi...@@ -99,7 +99,7 @@ fn choose(ui: &mut Ui, name: &str, names: &[&str], current: usize) -> Option<usi
99 ..Item::default()99 ..Item::default()
100 })100 })
101 .collect();101 .collect();
102 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());102 let anchor = Anchor::Below(combo);
103 ui::popup::menu(ui, menu, anchor, &items, None)103 ui::popup::menu(ui, menu, anchor, &items, None)
104}104}
105105
crates/snowbound/src/properties.rs+1-1
...@@ -175,7 +175,7 @@ impl State {...@@ -175,7 +175,7 @@ impl State {
175 ..Item::default()175 ..Item::default()
176 })176 })
177 .collect();177 .collect();
178 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());178 let anchor = Anchor::Below(combo);
179 if let Some(index) = ui::popup::menu(ui, colors(), anchor, &items, None) {179 if let Some(index) = ui::popup::menu(ui, colors(), anchor, &items, None) {
180 dialog.color = Some(SECTION_COLORS[index].0);180 dialog.color = Some(SECTION_COLORS[index].0);
181 }181 }
crates/snowbound/src/protection.rs+36-9
...@@ -5,7 +5,7 @@...@@ -5,7 +5,7 @@
5use crate::{Command, Library, State, art, manage::Structure};5use crate::{Command, Library, State, art, manage::Structure};
6use accesskit::Role;6use accesskit::Role;
7use std::{error::Error, sync::Arc, time::Duration};7use std::{error::Error, sync::Arc, time::Duration};
8use ui::{Anchor, Axis, Flags, Id, Spec, Ui, children, fill, px};8use ui::{Anchor, Axis, Extent, Flags, Id, Spec, Ui, children, fill, px};
9use winit::keyboard::NamedKey;9use winit::keyboard::NamedKey;
10use zeroize::Zeroizing;10use zeroize::Zeroizing;
1111
...@@ -133,6 +133,7 @@ impl State {...@@ -133,6 +133,7 @@ impl State {
133 shown,133 shown,
134 None,134 None,
135 None,135 None,
136 None,
136 section,137 section,
137 crate::TAB_ROW,138 crate::TAB_ROW,
138 strip,139 strip,
...@@ -165,24 +166,48 @@ impl State {...@@ -165,24 +166,48 @@ impl State {
165 let ui = &mut self.ui;166 let ui = &mut self.ui;
166 let entered =167 let entered =
167 ui::popup::navigation(ui, &[notice()], &[NamedKey::Enter]).contains(&NamedKey::Enter);168 ui::popup::navigation(ui, &[notice()], &[NamedKey::Enter]).contains(&NamedKey::Enter);
168 let area = ui.open(169 ui.open(
169 "locked",170 "locked",
170 Spec {171 Spec {
172 axis: Axis::Y,
171 size: [fill(), fill()],173 size: [fill(), fill()],
172 fill: Some(ui::mix(theme.paper, theme.chip, 0.6)),174 fill: Some(ui::mix(theme.paper, theme.chip, 0.6)),
173 ..Spec::default()175 ..Spec::default()
174 },176 },
175 );177 );
176 let [left, top, right, bottom] = ui.rect(area).unwrap_or_default();178 // A fifth of the way down, centred across, narrowing with a narrow page.
177 let width = 380.0_f32.min(right - left);179 let room = |ui: &mut Ui, part: &str, size| {
180 ui.leaf(
181 part,
182 Spec {
183 size,
184 ..Spec::default()
185 },
186 );
187 };
188 let fifth = Extent {
189 size: ui::Size::Fraction(0.2),
190 strictness: 1.0,
191 };
192 room(ui, "above", [fill(), fifth]);
193 ui.open(
194 "across",
195 Spec {
196 size: [fill(), children()],
197 ..Spec::default()
198 },
199 );
200 room(ui, "before", [fill(), px(0.0)]);
178 let open = ui.open_as(201 let open = ui.open_as(
179 notice(),202 notice(),
180 Spec {203 Spec {
181 flags: Flags::FLOAT | Flags::CLICKABLE | Flags::FOCUSABLE,204 flags: Flags::CLICKABLE | Flags::FOCUSABLE,
182 size: [px(width), children()],205 size: [
183 position: [206 Extent {
184 ((right - left - width) / 2.0).max(0.0),207 size: ui::Size::Pixels(380.0),
185 ((bottom - top) * 0.2).max(0.0),208 strictness: 0.0,
209 },
210 children(),
186 ],211 ],
187 gap: 12.0,212 gap: 12.0,
188 role: Some(Role::Button),213 role: Some(Role::Button),
...@@ -230,6 +255,8 @@ impl State {...@@ -230,6 +255,8 @@ impl State {
230 );255 );
231 ui.close();256 ui.close();
232 ui.close();257 ui.close();
258 room(ui, "after", [fill(), px(0.0)]);
259 ui.close();
233 ui.close();260 ui.close();
234 ui.theme = chrome;261 ui.theme = chrome;
235 if ui.signal(open).clicked || entered {262 if ui.signal(open).clicked || entered {
crates/snowbound/src/recording.rs+22-8
...@@ -7,7 +7,7 @@ use crate::{State, media, platform};...@@ -7,7 +7,7 @@ use crate::{State, media, platform};
7use canvas::recording::{RATE, video};7use canvas::recording::{RATE, video};
8use onestore::page::Attachment;8use onestore::page::Attachment;
9use std::{error::Error, ops::Range, path::Path, sync::Arc, time::Duration};9use std::{error::Error, ops::Range, path::Path, sync::Arc, time::Duration};
10use ui::{Axis, Flags, Spec, Theme, children, fit, px};10use ui::{Axis, Flags, Spec, Theme, children, fill, fit, px};
11use winit::keyboard::NamedKey;11use winit::keyboard::NamedKey;
1212
13/// What records or plays now.13/// What records or plays now.
...@@ -273,11 +273,7 @@ impl State {...@@ -273,11 +273,7 @@ impl State {
273273
274 /// The transport over the page's foot while something records or plays: OneNote's274 /// The transport over the page's foot while something records or plays: OneNote's
275 /// Recording and Playback tabs, with a video's pictures above.275 /// Recording and Playback tabs, with a video's pictures above.
276 pub(crate) fn transport(276 pub(crate) fn transport(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> {
277 &mut self,
278 theme: &Theme,
279 page: [f32; 4],
280 ) -> Result<(), Box<dyn Error>> {
281 let row = theme.font_size * 2.0;277 let row = theme.font_size * 2.0;
282 if matches!(self.media, Media::Saving { .. }) {278 if matches!(self.media, Media::Saving { .. }) {
283 self.stop_recording(false)?;279 self.stop_recording(false)?;
...@@ -354,13 +350,30 @@ impl State {...@@ -354,13 +350,30 @@ impl State {
354 .as_ref()350 .as_ref()
355 .map_or(0.0, |image| image.size()[1] as f32 + 6.0);351 .map_or(0.0, |image| image.size()[1] as f32 + 6.0);
356 let height = row + 16.0 + picture_height;352 let height = row + 16.0 + picture_height;
353 // In the page's lower leading corner.
354 self.ui.open(
355 "corner",
356 Spec {
357 flags: Flags::FLOAT,
358 axis: Axis::Y,
359 size: [fill(), fill()],
360 pad: [16.0, 16.0],
361 ..Spec::default()
362 },
363 );
364 self.ui.leaf(
365 "room",
366 Spec {
367 size: [px(0.0), fill()],
368 ..Spec::default()
369 },
370 );
357 self.ui.open(371 self.ui.open(
358 "transport",372 "transport",
359 Spec {373 Spec {
360 flags: Flags::FLOAT | Flags::CLICKABLE,374 flags: Flags::CLICKABLE,
361 axis: Axis::Y,375 axis: Axis::Y,
362 size: [children(), px(height)],376 size: [children(), px(height)],
363 position: [16.0, page[3] - page[1] - height - 16.0],
364 fill: Some(theme.popup),377 fill: Some(theme.popup),
365 shadow: Some(theme.shadow),378 shadow: Some(theme.shadow),
366 radius: 8.0,379 radius: 8.0,
...@@ -435,6 +448,7 @@ impl State {...@@ -435,6 +448,7 @@ impl State {
435 }448 }
436 self.ui.close();449 self.ui.close();
437 self.ui.close();450 self.ui.close();
451 self.ui.close();
438 if let Some(at) = seek452 if let Some(at) = seek
439 && let Media::Playing(playback) = &mut self.media453 && let Media::Playing(playback) = &mut self.media
440 {454 {
crates/snowbound/src/rename.rs+17-43
...@@ -54,6 +54,23 @@ pub fn field() -> Id {...@@ -54,6 +54,23 @@ pub fn field() -> Id {
54 Id::ROOT.child("rename")54 Id::ROOT.child("rename")
55}55}
5656
57/// The rename field over the label of a section tab `tall`, editing `name`, as `edit`.
58pub fn tab_field(ui: &mut Ui, theme: &Theme, name: &mut String, tall: f32) -> Option<bool> {
59 let width = ui.measure(name)[0] + 4.0 + 2.0 * PAD;
60 ui.open(
61 "rename",
62 Spec {
63 flags: ui::Flags::FLOAT,
64 size: [px(width.max(48.0)), px(tall - 6.0)],
65 position: [ui::shell::TAB_PAD - PAD, 3.0],
66 ..Spec::default()
67 },
68 );
69 let kept = edit(ui, theme, name, tall - 6.0);
70 ui.close();
71 kept
72}
73
57/// The rename field, `height` tall, editing `name`: whether Enter kept or Escape dropped74/// The rename field, `height` tall, editing `name`: whether Enter kept or Escape dropped
58/// the name typed.75/// the name typed.
59pub fn edit(ui: &mut Ui, theme: &Theme, name: &mut String, height: f32) -> Option<bool> {76pub fn edit(ui: &mut Ui, theme: &Theme, name: &mut String, height: f32) -> Option<bool> {
...@@ -146,49 +163,6 @@ impl State {...@@ -146,49 +163,6 @@ impl State {
146 }163 }
147 }164 }
148165
149 /// The rename field over the section tab being renamed, of those built as `row` in the
150 /// tab row `bar`, where the tab's label stands.
151 pub(crate) fn tab_rename_field(&mut self, theme: &Theme, row: Id, bar: Id) {
152 let (Some(session), Some(renaming)) = (&self.session, &mut self.renaming) else {
153 return;
154 };
155 let Some(tab) = session
156 .tabs
157 .iter()
158 .position(|tab| renaming.entry(&session.library, &tab.path, true))
159 else {
160 return;
161 };
162 let (Some(rect), Some(bar)) =
163 (self.ui.rect(ui::shell::tab_id(row, tab)), self.ui.rect(bar))
164 else {
165 return;
166 };
167 let width = self.ui.measure(&renaming.name)[0] + 4.0 + 2.0 * PAD;
168 self.ui.open(
169 "rename",
170 Spec {
171 flags: ui::Flags::FLOAT,
172 size: [px(width.max(48.0)), px(rect[3] - rect[1] - 6.0)],
173 position: [
174 rect[0] + ui::shell::TAB_PAD - PAD - bar[0],
175 rect[1] + 3.0 - bar[1],
176 ],
177 ..Spec::default()
178 },
179 );
180 let kept = edit(
181 &mut self.ui,
182 theme,
183 &mut renaming.name,
184 rect[3] - rect[1] - 6.0,
185 );
186 self.ui.close();
187 if let Some(keep) = kept {
188 self.finish_renaming(keep);
189 }
190 }
191
192 /// Gives page `space` of the open section the title `name`.166 /// Gives page `space` of the open section the title `name`.
193 fn retitle(&mut self, space: ExGuid, name: String) -> Result<(), Box<dyn Error>> {167 fn retitle(&mut self, space: ExGuid, name: String) -> Result<(), Box<dyn Error>> {
194 self.persist()?;168 self.persist()?;
crates/snowbound/src/save_as.rs+1-1
...@@ -265,7 +265,7 @@ fn choose(ui: &mut ui::Ui, label: &str, names: &[&str], current: usize) -> Optio...@@ -265,7 +265,7 @@ fn choose(ui: &mut ui::Ui, label: &str, names: &[&str], current: usize) -> Optio
265 ..Item::default()265 ..Item::default()
266 })266 })
267 .collect();267 .collect();
268 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());268 let anchor = Anchor::Below(combo);
269 let chosen = ui::popup::menu(ui, menu, anchor, &items, None);269 let chosen = ui::popup::menu(ui, menu, anchor, &items, None);
270 ui.close();270 ui.close();
271 chosen271 chosen
crates/snowbound/src/search.rs+9-14
...@@ -780,8 +780,7 @@ impl State {...@@ -780,8 +780,7 @@ impl State {
780 self.ui.close();780 self.ui.close();
781 // Results opened this frame build from the next, as the box's field was built.781 // Results opened this frame build from the next, as the box's field was built.
782 if searching || !self.ui.popup_open(results()) {782 if searching || !self.ui.popup_open(results()) {
783 let rect = self.ui.rect(box_id).unwrap_or_default();783 self.results_popup(theme, box_id)?;
784 self.results_popup(theme, rect)?;
785 }784 }
786 self.scope_popup();785 self.scope_popup();
787 Ok(())786 Ok(())
...@@ -791,8 +790,7 @@ impl State {...@@ -791,8 +790,7 @@ impl State {
791 let button = self.ui.id("scope");790 let button = self.ui.id("scope");
792 if ui::shell::tool_button(&mut self.ui, "scope", art::SEARCH, theme.text_dim, None).pressed791 if ui::shell::tool_button(&mut self.ui, "scope", art::SEARCH, theme.text_dim, None).pressed
793 {792 {
794 let rect = self.ui.rect(button).unwrap_or_default();793 self.search.anchor = Some(Anchor::Below(button));
795 self.search.anchor = Some(Anchor::Below(rect));
796 self.ui.open_popup(scope_menu());794 self.ui.open_popup(scope_menu());
797 }795 }
798 let open = self.ui.popup_open(scope_menu());796 let open = self.ui.popup_open(scope_menu());
...@@ -856,7 +854,7 @@ impl State {...@@ -856,7 +854,7 @@ impl State {
856 }854 }
857855
858 /// The dropdown of results, over the box it drops from.856 /// The dropdown of results, over the box it drops from.
859 fn results_popup(&mut self, theme: &Theme, rect: [f32; 4]) -> Result<(), Box<dyn Error>> {857 fn results_popup(&mut self, theme: &Theme, over: Id) -> Result<(), Box<dyn Error>> {
860 if !self.ui.popup_open(results()) {858 if !self.ui.popup_open(results()) {
861 // Closed by Esc or a press elsewhere: the search ends.859 // Closed by Esc or a press elsewhere: the search ends.
862 if std::mem::take(&mut self.search.open) {860 if std::mem::take(&mut self.search.open) {
...@@ -879,21 +877,18 @@ impl State {...@@ -879,21 +877,18 @@ impl State {
879 if self.ui.focused() == Some(results()) {877 if self.ui.focused() == Some(results()) {
880 self.ui.set_focus(Some(field()));878 self.ui.set_focus(Some(field()));
881 }879 }
882 let [left, top, right, bottom] = rect;
883 let anchor = Anchor::Over([left - 4.0, top - 4.0, right + 4.0, bottom + 4.0]);
884 let width = RESULTS.max(right - left);
885 self.ui.open_as(880 self.ui.open_as(
886 results(),881 results(),
887 Spec {882 Spec {
888 axis: Axis::Y,883 axis: Axis::Y,
889 size: [px(width), children()],884 size: [px(RESULTS), children()],
890 fill: Some(theme.popup),885 fill: Some(theme.popup),
891 border: Some(theme.chip),886 border: Some(theme.chip),
892 shadow: Some(theme.shadow),887 shadow: Some(theme.shadow),
893 radius: 6.0,888 radius: 6.0,
894 pad: [4.0, 4.0],889 pad: [4.0, 4.0],
895 gap: 4.0,890 gap: 4.0,
896 anchor: Some(anchor),891 anchor: Some(Anchor::Over(over)),
897 role: Some(Role::Dialog),892 role: Some(Role::Dialog),
898 ..Spec::default()893 ..Spec::default()
899 },894 },
...@@ -985,8 +980,7 @@ impl State {...@@ -985,8 +980,7 @@ impl State {
985 )980 )
986 .clicked981 .clicked
987 {982 {
988 let rect = self.ui.rect(change).unwrap_or_default();983 self.search.anchor = Some(Anchor::Below(change));
989 self.search.anchor = Some(Anchor::Below(rect));
990 self.ui.open_popup(scope_menu());984 self.ui.open_popup(scope_menu());
991 }985 }
992 self.ui.leaf(986 self.ui.leaf(
...@@ -1016,8 +1010,9 @@ impl State {...@@ -1016,8 +1010,9 @@ impl State {
1016 count: found.len(),1010 count: found.len(),
1017 };1011 };
1018 let content = found.len() as f32 * RESULT + ui::Rows::space_before(&rows, found.len());1012 let content = found.len() as f32 * RESULT + ui::Rows::space_before(&rows, found.len());
1019 let window = self.ui.rect(Id::ROOT).map_or(600.0, |window| window[3]);1013 // Down to the window's foot from the box, as last laid out.
1020 let view = content.min((window - bottom - 80.0).max(RESULT));1014 let bottom = self.ui.laid_out(over).map_or(0.0, |rect| rect[3]);
1015 let view = content.min((self.ui.size()[1] - bottom - 80.0).max(RESULT));
1021 let before = self.search.selected;1016 let before = self.search.selected;
1022 let places: Vec<String> = found1017 let places: Vec<String> = found
1023 .iter()1018 .iter()
crates/snowbound/src/sidebar.rs+62-24
...@@ -298,7 +298,7 @@ fn sidebar(...@@ -298,7 +298,7 @@ fn sidebar(
298 }298 }
299 if row.dragging299 if row.dragging
300 && !renamed300 && !renamed
301 && let Some(rect) = ui.rect(id)301 && let Some(rect) = ui.laid_out(id)
302 {302 {
303 tree.held_notebook = Some(Entry {303 tree.held_notebook = Some(Entry {
304 notebook: index,304 notebook: index,
...@@ -316,7 +316,7 @@ fn sidebar(...@@ -316,7 +316,7 @@ fn sidebar(
316 ui.close();316 ui.close();
317 }317 }
318 ui.close();318 ui.close();
319 tree.blocks.extend(ui.rect(block));319 tree.blocks.extend(ui.laid_out(block));
320 }320 }
321 if let Some((_, slot)) = tree.lifted_notebook {321 if let Some((_, slot)) = tree.lifted_notebook {
322 opening(ui, tree, ("notebook gap", ""), slot == place);322 opening(ui, tree, ("notebook gap", ""), slot == place);
...@@ -632,7 +632,7 @@ fn opening(ui: &mut Ui, tree: &mut Tree, part: impl std::hash::Hash + Copy, here...@@ -632,7 +632,7 @@ fn opening(ui: &mut Ui, tree: &mut Tree, part: impl std::hash::Hash + Copy, here
632 let id = ui.id(part);632 let id = ui.id(part);
633 let height = ui.animate(id, if here { ROW } else { 0.0 });633 let height = ui.animate(id, if here { ROW } else { 0.0 });
634 if here {634 if here {
635 tree.gap = ui.rect(id);635 tree.gap = ui.laid_out(id);
636 }636 }
637 if height > 0.0 {637 if height > 0.0 {
638 ui.leaf(638 ui.leaf(
...@@ -657,7 +657,7 @@ fn unfolding(ui: &mut Ui, part: impl std::hash::Hash, unfolded: bool) -> bool {...@@ -657,7 +657,7 @@ fn unfolding(ui: &mut Ui, part: impl std::hash::Hash, unfolded: bool) -> bool {
657 ui::children()657 ui::children()
658 } else {658 } else {
659 let rows = ui659 let rows = ui
660 .rect(outer.child("rows"))660 .laid_out(outer.child("rows"))
661 .map_or(0.0, |rect| rect[3] - rect[1]);661 .map_or(0.0, |rect| rect[3] - rect[1]);
662 let height = ui.animate(outer, if unfolded { rows } else { 0.0 });662 let height = ui.animate(outer, if unfolded { rows } else { 0.0 });
663 if !unfolded && height == 0.0 {663 if !unfolded && height == 0.0 {
...@@ -687,7 +687,7 @@ fn unfolding(ui: &mut Ui, part: impl std::hash::Hash, unfolded: bool) -> bool {...@@ -687,7 +687,7 @@ fn unfolding(ui: &mut Ui, part: impl std::hash::Hash, unfolded: bool) -> bool {
687687
688/// Records a section or group row, built as `id`, for dragging.688/// Records a section or group row, built as `id`, for dragging.
689fn entry(ui: &Ui, tree: &mut Tree, notebook: usize, path: &str, id: Id, group: bool, row: &Signal) {689fn entry(ui: &Ui, tree: &mut Tree, notebook: usize, path: &str, id: Id, group: bool, row: &Signal) {
690 let Some(rect) = ui.rect(id) else {690 let Some(rect) = ui.laid_out(id) else {
691 return;691 return;
692 };692 };
693 let entry = Entry {693 let entry = Entry {
...@@ -929,7 +929,7 @@ impl crate::State {...@@ -929,7 +929,7 @@ impl crate::State {
929 gap,929 gap,
930 ..930 ..
931 } = tree;931 } = tree;
932 let corner = self.ui.rect(rows_id).unwrap_or_default();932 let corner = self.ui.laid_out(rows_id).unwrap_or_default();
933 let found = ghost.is_some();933 let found = ghost.is_some();
934 let (ghost_held, settled) = self.lifted_row(theme, ghost, gap, &rows, corner);934 let (ghost_held, settled) = self.lifted_row(theme, ghost, gap, &rows, corner);
935 let settled = settled || !found;935 let settled = settled || !found;
...@@ -987,17 +987,9 @@ impl crate::State {...@@ -987,17 +987,9 @@ impl crate::State {
987 if self.temporary {987 if self.temporary {
988 return;988 return;
989 }989 }
990 let (nav, toggle) = if self.navigation_bar_right {990 let far = self.navigation_bar_right;
991 // The notebook button stands at the body's far edge, as laid out last frame.991 let toggle = (MARGIN + ROW_PAD - (RAIL - ICON) / 2.0) * width / WIDTH;
992 let edge = self992 let nav = if far { 0.0 } else { toggle + RAIL };
993 .ui
994 .rect(self.ui.current())
995 .map_or(0.0, |[left, _, right, _]| right - left - RAIL);
996 (0.0, edge)
997 } else {
998 let toggle = (MARGIN + ROW_PAD - (RAIL - ICON) / 2.0) * width / WIDTH;
999 (toggle + RAIL, toggle)
1000 };
1001 let pad = [993 let pad = [
1002 (RAIL - ui::shell::TOOL) / 2.0,994 (RAIL - ui::shell::TOOL) / 2.0,
1003 (RAIL - ui::shell::TOOL) / 2.0 + DROP,995 (RAIL - ui::shell::TOOL) / 2.0 + DROP,
...@@ -1021,10 +1013,32 @@ impl crate::State {...@@ -1021,10 +1013,32 @@ impl crate::State {
1021 }1013 }
1022 }1014 }
1023 self.ui.close();1015 self.ui.close();
1016 // The notebook button stands at the body's far edge, after the room before it.
1017 if far {
1018 self.ui.open(
1019 "far edge",
1020 Spec {
1021 flags: Flags::FLOAT,
1022 size: [fill(), px(height)],
1023 ..Spec::default()
1024 },
1025 );
1026 self.ui.leaf(
1027 "room",
1028 Spec {
1029 size: [fill(), px(height)],
1030 ..Spec::default()
1031 },
1032 );
1033 }
1024 self.ui.open(1034 self.ui.open(
1025 "toggle",1035 "toggle",
1026 Spec {1036 Spec {
1027 flags: Flags::FLOAT | Flags::CLIP,1037 flags: if far {
1038 Flags::CLIP
1039 } else {
1040 Flags::FLOAT | Flags::CLIP
1041 },
1028 size: [px(RAIL), px(height)],1042 size: [px(RAIL), px(height)],
1029 position: [toggle, 0.0],1043 position: [toggle, 0.0],
1030 pad,1044 pad,
...@@ -1043,6 +1057,9 @@ impl crate::State {...@@ -1043,6 +1057,9 @@ impl crate::State {
1043 }1057 }
1044 crate::tip(&mut self.ui, Cmd::Sidebar);1058 crate::tip(&mut self.ui, Cmd::Sidebar);
1045 self.ui.close();1059 self.ui.close();
1060 if far {
1061 self.ui.close();
1062 }
1046 }1063 }
10471064
1048 /// The dragged row, `ghost`, drawn over the others in the rows' box `corner`: following1065 /// The dragged row, `ghost`, drawn over the others in the rows' box `corner`: following
...@@ -1499,25 +1516,43 @@ impl crate::State {...@@ -1499,25 +1516,43 @@ impl crate::State {
1499 const BUTTON: [f32; 2] = [240.0, 32.0];1516 const BUTTON: [f32; 2] = [240.0, 32.0];
1500 let theme = self.page_area_theme();1517 let theme = self.page_area_theme();
1501 let chrome = std::mem::replace(&mut self.ui.theme, theme.clone());1518 let chrome = std::mem::replace(&mut self.ui.theme, theme.clone());
1502 let [left, top, right, bottom] = self.ui.rect(id).unwrap_or_default();
1503 self.ui.open_as(1519 self.ui.open_as(
1504 id,1520 id,
1505 Spec {1521 Spec {
1522 axis: Axis::Y,
1506 size: [fill(), fill()],1523 size: [fill(), fill()],
1507 fill: Some(theme.paper),1524 fill: Some(theme.paper),
1508 ..Spec::default()1525 ..Spec::default()
1509 },1526 },
1510 );1527 );
1528 // Centred across, with two fifths of the room to spare above it and the rest below.
1529 let room = |ui: &mut Ui, part: &str, size| {
1530 ui.leaf(
1531 part,
1532 Spec {
1533 size,
1534 ..Spec::default()
1535 },
1536 );
1537 };
1538 let share = |fraction| ui::Extent {
1539 size: ui::Size::Fraction(fraction),
1540 strictness: 0.0,
1541 };
1542 room(&mut self.ui, "above", [fill(), share(0.4)]);
1543 self.ui.open(
1544 "across",
1545 Spec {
1546 size: [fill(), ui::children()],
1547 ..Spec::default()
1548 },
1549 );
1550 room(&mut self.ui, "before", [fill(), px(0.0)]);
1511 self.ui.open(1551 self.ui.open(
1512 "content",1552 "content",
1513 Spec {1553 Spec {
1514 flags: Flags::FLOAT,
1515 axis: Axis::Y,1554 axis: Axis::Y,
1516 size: [px(BUTTON[0]), ui::children()],1555 size: [px(BUTTON[0]), ui::children()],
1517 position: [
1518 ((right - left - BUTTON[0]) / 2.0).max(0.0),
1519 ((bottom - top) * 0.4 - 60.0).max(0.0),
1520 ],
1521 gap: 10.0,1556 gap: 10.0,
1522 ..Spec::default()1557 ..Spec::default()
1523 },1558 },
...@@ -1558,6 +1593,9 @@ impl crate::State {...@@ -1558,6 +1593,9 @@ impl crate::State {
1558 self.commands.extend(chosen);1593 self.commands.extend(chosen);
1559 let saved = crate::server::saved_servers(&mut self.ui, servers);1594 let saved = crate::server::saved_servers(&mut self.ui, servers);
1560 self.ui.close();1595 self.ui.close();
1596 room(&mut self.ui, "after", [fill(), px(0.0)]);
1597 self.ui.close();
1598 room(&mut self.ui, "below", [fill(), share(0.6)]);
1561 self.ui.close();1599 self.ui.close();
1562 self.ui.theme = chrome;1600 self.ui.theme = chrome;
1563 if let Some(saved) = saved {1601 if let Some(saved) = saved {
crates/snowbound/src/symbol.rs+1-1
...@@ -547,7 +547,7 @@ impl State {...@@ -547,7 +547,7 @@ impl State {
547 ui.close();547 ui.close();
548 ui.close();548 ui.close();
549549
550 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());550 let anchor = Anchor::Below(combo);
551 if ui.popup_open(subsets()) {551 if ui.popup_open(subsets()) {
552 // Each subset that shows a character, and its first.552 // Each subset that shows a character, and its first.
553 let all = characters(ui);553 let all = characters(ui);
crates/snowbound/src/sync.rs+10-13
...@@ -290,7 +290,7 @@ fn changes_list(ui: &mut Ui, changes: &update::Changes, listed: bool) {...@@ -290,7 +290,7 @@ fn changes_list(ui: &mut Ui, changes: &update::Changes, listed: bool) {
290 // Unfolds to the titles' height, scrolling past a few dozen lines.290 // Unfolds to the titles' height, scrolling past a few dozen lines.
291 let list = ui.id("changes");291 let list = ui.id("changes");
292 let rows = ui292 let rows = ui
293 .rect(list.child("rows"))293 .laid_out(list.child("rows"))
294 .map_or(0.0, |rect| rect[3] - rect[1]);294 .map_or(0.0, |rect| rect[3] - rect[1]);
295 let height = ui.animate(list, if listed { rows.min(line * 9.0) } else { 0.0 });295 let height = ui.animate(list, if listed { rows.min(line * 9.0) } else { 0.0 });
296 if listed || height > 0.0 {296 if listed || height > 0.0 {
...@@ -533,7 +533,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {...@@ -533,7 +533,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
533 let state = sync.state();533 let state = sync.state();
534 let syncing = !facts.offline && state == SyncState::Syncing;534 let syncing = !facts.offline && state == SyncState::Syncing;
535 let bar = id().child("status").child("bar");535 let bar = id().child("status").child("bar");
536 let shown = ui.rect(bar).is_some() && state == SyncState::UpToDate;536 let shown = ui.laid_out(bar).is_some() && state == SyncState::UpToDate;
537 let showing = !facts.offline && shows_sync(ui, button(), syncing, shown);537 let showing = !facts.offline && shows_sync(ui, button(), syncing, shown);
538 let (mut headline, mut icon) = describe(&sync, facts.offline, !showing);538 let (mut headline, mut icon) = describe(&sync, facts.offline, !showing);
539 let host = facts.place.first().map_or("the notebook", String::as_str);539 let host = facts.place.first().map_or("the notebook", String::as_str);
...@@ -731,7 +731,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {...@@ -731,7 +731,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
731 ..Spec::default()731 ..Spec::default()
732 },732 },
733 );733 );
734 let width = ui.rect(bar).map_or(0.0, |rect| rect[2] - rect[0]);734 let width = ui.laid_out(bar).map_or(0.0, |rect| rect[2] - rect[0]);
735 let filled = width * fraction;735 let filled = width * fraction;
736 ui.mark([0.0, 0.0, filled, 6.0], theme.accent, 3.0);736 ui.mark([0.0, 0.0, filled, 6.0], theme.accent, 3.0);
737 // A sheen sweeps the filled part while the sync runs, so a long section still moves.737 // A sheen sweeps the filled part while the sync runs, so a long section still moves.
...@@ -1075,10 +1075,7 @@ impl State {...@@ -1075,10 +1075,7 @@ impl State {
1075 session.sync = session.section.sync_status()?;1075 session.sync = session.section.sync_status()?;
1076 let (library, anchor) = match &self.sync_notebook {1076 let (library, anchor) = match &self.sync_notebook {
1077 Some((library, at)) => (Arc::clone(library), Anchor::Point(*at)),1077 Some((library, at)) => (Arc::clone(library), Anchor::Point(*at)),
1078 None => (1078 None => (Arc::clone(&session.library), Anchor::Below(button())),
1079 Arc::clone(&session.library),
1080 Anchor::Below(ui.rect(button()).unwrap_or_default()),
1081 ),
1082 };1079 };
1083 let own = Arc::ptr_eq(&library, &session.library);1080 let own = Arc::ptr_eq(&library, &session.library);
1084 let offline = library::offline();1081 let offline = library::offline();
...@@ -1244,7 +1241,7 @@ mod tests {...@@ -1244,7 +1241,7 @@ mod tests {
1244 if !ui.popup_open(id()) {1241 if !ui.popup_open(id()) {
1245 ui.open_popup(id());1242 ui.open_popup(id());
1246 }1243 }
1247 open(ui, Anchor::Below(ui.rect(button()).unwrap_or_default()));1244 open(ui, Anchor::Below(button()));
1248 build(ui, facts);1245 build(ui, facts);
1249 ui.close();1246 ui.close();
1250 ui.end();1247 ui.end();
...@@ -1352,7 +1349,7 @@ mod tests {...@@ -1352,7 +1349,7 @@ mod tests {
1352 .unwrap();1349 .unwrap();
1353 let mapped = buffer.get_mapped_range(..).unwrap();1350 let mapped = buffer.get_mapped_range(..).unwrap();
1354 let bottom = bottom1351 let bottom = bottom
1355 .unwrap_or(ui.rect(id()).unwrap()[3] + 24.0)1352 .unwrap_or(ui.laid_out(id()).unwrap()[3] + 24.0)
1356 .min(SIZE[1]);1353 .min(SIZE[1]);
1357 let height = (bottom * SCALE) as u32;1354 let height = (bottom * SCALE) as u32;
1358 let pixels: Vec<u8> = mapped1355 let pixels: Vec<u8> = mapped
...@@ -1382,7 +1379,7 @@ mod tests {...@@ -1382,7 +1379,7 @@ mod tests {
1382 }1379 }
13831380
1384 fn bar(ui: &Ui) -> bool {1381 fn bar(ui: &Ui) -> bool {
1385 ui.rect(id().child("status").child("bar")).is_some()1382 ui.laid_out(id().child("status").child("bar")).is_some()
1386 }1383 }
13871384
1388 #[test]1385 #[test]
...@@ -1547,7 +1544,7 @@ mod tests {...@@ -1547,7 +1544,7 @@ mod tests {
1547 let mut ui = ui(Appearance::Light);1544 let mut ui = ui(Appearance::Light);
1548 let mut now = Instant::now();1545 let mut now = Instant::now();
1549 settle(&mut ui, &mut now, &made());1546 settle(&mut ui, &mut now, &made());
1550 let [_, top, _, bottom] = ui.rect(id()).unwrap();1547 let [_, top, _, bottom] = ui.laid_out(id()).unwrap();
1551 (name, bottom - top)1548 (name, bottom - top)
1552 })1549 })
1553 .collect();1550 .collect();
...@@ -1570,7 +1567,7 @@ mod tests {...@@ -1570,7 +1567,7 @@ mod tests {
1570 let mut now = Instant::now();1567 let mut now = Instant::now();
1571 let facts = Facts { offline, ..made() };1568 let facts = Facts { offline, ..made() };
1572 settle(&mut ui, &mut now, &facts);1569 settle(&mut ui, &mut now, &facts);
1573 places.push((ui.rect(switch).unwrap(), ui.rect(controls).unwrap()));1570 places.push((ui.laid_out(switch).unwrap(), ui.laid_out(controls).unwrap()));
1574 }1571 }
1575 assert!(1572 assert!(
1576 places.windows(2).all(|pair| pair[0] == pair[1]),1573 places.windows(2).all(|pair| pair[0] == pair[1]),
...@@ -1594,7 +1591,7 @@ mod tests {...@@ -1594,7 +1591,7 @@ mod tests {
1594 let mut now = Instant::now();1591 let mut now = Instant::now();
1595 let facts = facts();1592 let facts = facts();
1596 settle(&mut ui, &mut now, &facts);1593 settle(&mut ui, &mut now, &facts);
1597 assert!(ui.rect(id()).is_some(), "{name}");1594 assert!(ui.laid_out(id()).is_some(), "{name}");
1598 if let (Some(output), Some(gpu)) = (&output, &mut gpu) {1595 if let (Some(output), Some(gpu)) = (&output, &mut gpu) {
1599 gpu.save(&ui, &output.join(format!("{name}-{theme}.png")), None);1596 gpu.save(&ui, &output.join(format!("{name}-{theme}.png")), None);
1600 }1597 }
crates/snowbound/src/tags.rs+1-1
...@@ -341,7 +341,7 @@ fn picker(...@@ -341,7 +341,7 @@ fn picker(
341 if ui.signal(button).pressed {341 if ui.signal(button).pressed {
342 ui.open_popup(menu);342 ui.open_popup(menu);
343 }343 }
344 Anchor::Below(ui.rect(button).unwrap_or_default())344 Anchor::Below(button)
345}345}
346346
347/// The frame of a dialog titled `title`, centred in the window.347/// The frame of a dialog titled `title`, centred in the window.
crates/snowbound/src/templates.rs+8-6
...@@ -112,6 +112,10 @@ fn strip(...@@ -112,6 +112,10 @@ fn strip(
112 shown.push(pick);112 shown.push(pick);
113 }113 }
114 shown.push(more);114 shown.push(more);
115 let tiles = (shown.iter())
116 .map(|(_, label)| GAP + tile_width(ui, label))
117 .sum::<f32>()
118 - GAP;
115 let mut chosen = None;119 let mut chosen = None;
116 ui.open(120 ui.open(
117 "templates",121 "templates",
...@@ -177,15 +181,12 @@ fn strip(...@@ -177,15 +181,12 @@ fn strip(
177 }181 }
178 ui.close();182 ui.close();
179 }183 }
180 let width = ui
181 .rect(ui.id("tiles"))
182 .map_or(0.0, |rect| rect[2] - rect[0]);
183 let close = ui.leaf(184 let close = ui.leaf(
184 "dismiss",185 "dismiss",
185 Spec {186 Spec {
186 flags: Flags::FLOAT | Flags::CLICKABLE,187 flags: Flags::FLOAT | Flags::CLICKABLE,
187 size: [px(22.0), px(22.0)],188 size: [px(22.0), px(22.0)],
188 position: [width + 2.0 * PAD - 14.0, -8.0],189 position: [tiles + 2.0 * PAD - 14.0, -8.0],
189 icon: Some(art::CLOSE),190 icon: Some(art::CLOSE),
190 color: Some(theme.text_dim),191 color: Some(theme.text_dim),
191 fill: Some(theme.popup),192 fill: Some(theme.popup),
...@@ -437,7 +438,8 @@ impl crate::State {...@@ -437,7 +438,8 @@ impl crate::State {
437 self.templates = View::Strip;438 self.templates = View::Strip;
438 return;439 return;
439 }440 }
440 let (Some(rect), Some(start)) = (self.ui.rect(crate::page()), editor.body_start()) else {441 let (Some(rect), Some(start)) = (self.ui.laid_out(crate::page()), editor.body_start())
442 else {
441 return;443 return;
442 };444 };
443 let viewport = self.view.viewport;445 let viewport = self.view.viewport;
...@@ -503,7 +505,7 @@ impl crate::State {...@@ -503,7 +505,7 @@ impl crate::State {
503 /// The Page Color menu's Show All: every template's art, one of which goes behind the505 /// The Page Color menu's Show All: every template's art, one of which goes behind the
504 /// open page.506 /// open page.
505 fn art_gallery(&mut self, theme: &Theme) {507 fn art_gallery(&mut self, theme: &Theme) {
506 let Some(rect) = self.ui.rect(crate::page()) else {508 let Some(rect) = self.ui.laid_out(crate::page()) else {
507 return;509 return;
508 };510 };
509 let paper = Paper {511 let paper = Paper {
crates/snowbound/src/themes.rs+6-6
...@@ -446,7 +446,7 @@ impl State {...@@ -446,7 +446,7 @@ impl State {
446 ..Item::default()446 ..Item::default()
447 })447 })
448 .collect();448 .collect();
449 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());449 let anchor = Anchor::Over(combo);
450 if let Some(index) = ui::popup::menu(ui, popup("styles"), anchor, &items, None) {450 if let Some(index) = ui::popup::menu(ui, popup("styles"), anchor, &items, None) {
451 dialog.style = index;451 dialog.style = index;
452 }452 }
...@@ -475,7 +475,7 @@ impl State {...@@ -475,7 +475,7 @@ impl State {
475 ..Item::default()475 ..Item::default()
476 })476 })
477 .collect();477 .collect();
478 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());478 let anchor = Anchor::Over(combo);
479 if let Some(index) = ui::popup::menu(ui, popup("fonts"), anchor, &items, Some("Font")) {479 if let Some(index) = ui::popup::menu(ui, popup("fonts"), anchor, &items, Some("Font")) {
480 let font = names[index].to_owned();480 let font = names[index].to_owned();
481 change = Some(Change::Font(font));481 change = Some(Change::Font(font));
...@@ -495,7 +495,7 @@ impl State {...@@ -495,7 +495,7 @@ impl State {
495 ..Item::default()495 ..Item::default()
496 })496 })
497 .collect();497 .collect();
498 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());498 let anchor = Anchor::Over(combo);
499 if let Some(index) = ui::popup::menu(ui, popup("sizes"), anchor, &items, None) {499 if let Some(index) = ui::popup::menu(ui, popup("sizes"), anchor, &items, None) {
500 let size = crate::SIZES[index];500 let size = crate::SIZES[index];
501 change = Some(Change::Size(size));501 change = Some(Change::Size(size));
...@@ -521,7 +521,7 @@ impl State {...@@ -521,7 +521,7 @@ impl State {
521 .iter()521 .iter()
522 .map(|&(color, name)| (canvas::gpu::colorref(color), name))522 .map(|&(color, name)| (canvas::gpu::colorref(color), name))
523 .collect();523 .collect();
524 let anchor = Anchor::Below(ui.rect(button).unwrap_or_default());524 let anchor = Anchor::Below(button);
525 let buttons = ["Automatic", "Theme"];525 let buttons = ["Automatic", "Theme"];
526 if let Some(picked) =526 if let Some(picked) =
527 ui::popup::color_grid(ui, popup("colors"), anchor, &buttons, &swatches, 10)527 ui::popup::color_grid(ui, popup("colors"), anchor, &buttons, &swatches, 10)
...@@ -556,7 +556,7 @@ impl State {...@@ -556,7 +556,7 @@ impl State {
556 ..Item::default()556 ..Item::default()
557 })557 })
558 .collect();558 .collect();
559 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());559 let anchor = Anchor::Over(combo);
560 if let Some(index) = ui::popup::menu(ui, popup(part), anchor, &items, None) {560 if let Some(index) = ui::popup::menu(ui, popup(part), anchor, &items, None) {
561 let points = SPACING[index];561 let points = SPACING[index];
562 change = Some(Change::Spacing(part == "before", points));562 change = Some(Change::Spacing(part == "before", points));
...@@ -634,7 +634,7 @@ impl State {...@@ -634,7 +634,7 @@ impl State {
634 ..Item::default()634 ..Item::default()
635 })635 })
636 .collect();636 .collect();
637 let anchor = Anchor::Below(ui.rect(ui.id(part)).unwrap_or_default());637 let anchor = Anchor::Below(ui.id(part));
638 ui::popup::menu(ui, menu, anchor, &items, None)638 ui::popup::menu(ui, menu, anchor, &items, None)
639 };639 };
640 let remove = match targets[..] {640 let remove = match targets[..] {
crates/snowbound/src/unpack.rs+1-1
...@@ -138,7 +138,7 @@ impl State {...@@ -138,7 +138,7 @@ impl State {
138 ..Item::default()138 ..Item::default()
139 })139 })
140 .collect();140 .collect();
141 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());141 let anchor = Anchor::Below(combo);
142 if let Some(index) = ui::popup::menu(ui, colors, anchor, &items, None) {142 if let Some(index) = ui::popup::menu(ui, colors, anchor, &items, None) {
143 dialog.color = Some(SECTION_COLORS[index].0);143 dialog.color = Some(SECTION_COLORS[index].0);
144 }144 }
crates/ui/examples/popups.rs+17-35
...@@ -75,8 +75,7 @@ fn main() {...@@ -75,8 +75,7 @@ fn main() {
75 ui::popup::menu(ui, context, Anchor::Point([140.0, 70.0]), &items[..8], None);75 ui::popup::menu(ui, context, Anchor::Point([140.0, 70.0]), &items[..8], None);
76 }),76 }),
77 ("combo", fonts, &|ui, [combo, _]| {77 ("combo", fonts, &|ui, [combo, _]| {
78 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());78 ui::popup::menu(ui, fonts, Anchor::Over(combo), &items, Some("Calibri"));
79 ui::popup::menu(ui, fonts, anchor, &items, Some("Calibri"));
80 }),79 }),
81 ("search", search, &|ui, [combo, _]| {80 ("search", search, &|ui, [combo, _]| {
82 results(ui, search, combo)81 results(ui, search, combo)
...@@ -138,33 +137,27 @@ fn results(ui: &mut Ui, id: Id, combo: Id) {...@@ -138,33 +137,27 @@ fn results(ui: &mut Ui, id: Id, combo: Id) {
138 return;137 return;
139 }138 }
140 let theme = ui.theme.clone();139 let theme = ui.theme.clone();
141 let [left, top, right, bottom] = ui.rect(combo).unwrap_or_default();
142 let anchor = Anchor::Over([left - 4.0, top - 4.0, right + 4.0, bottom + 4.0]);
143 let width = 300.0;
144 let open = ui.opening(id, anchor).unwrap_or(1.0);
145 ui.open_as(140 ui.open_as(
146 id,141 id,
147 Spec {142 Spec {
148 axis: ui::Axis::Y,143 axis: ui::Axis::Y,
149 size: [px(width), ui::children()],144 size: [px(300.0), ui::children()],
150 fill: Some(theme.popup),145 fill: Some(theme.popup),
151 border: Some(theme.chip),146 border: Some(theme.chip),
152 shadow: Some(theme.shadow),147 shadow: Some(theme.shadow),
153 radius: 6.0,148 radius: 6.0,
154 pad: [4.0, 4.0],149 pad: [4.0, 4.0],
155 gap: 4.0,150 gap: 4.0,
156 anchor: Some(anchor),151 anchor: Some(Anchor::Over(combo)),
157 ..Spec::default()152 ..Spec::default()
158 },153 },
159 );154 );
155 // Stands in for the combo, as tall, widening with the panel.
160 ui.leaf(156 ui.leaf(
161 "box",157 "box",
162 Spec {158 Spec {
163 flags: ui::Flags::STILL,159 flags: ui::Flags::STILL,
164 size: [160 size: [fill(), px(0.0)],
165 px(right - left + (width - 8.0 - (right - left)) * open),
166 px(bottom - top),
167 ],
168 text: Some("Search All Notebooks (⌘E)"),161 text: Some("Search All Notebooks (⌘E)"),
169 color: Some(theme.text_dim),162 color: Some(theme.text_dim),
170 fill: Some(theme.base),163 fill: Some(theme.base),
...@@ -253,8 +246,7 @@ fn keystroke(...@@ -253,8 +246,7 @@ fn keystroke(
253 let items = font_items();246 let items = font_items();
254 let id = Id::ROOT.child("fonts");247 let id = Id::ROOT.child("fonts");
255 let build = |ui: &mut Ui, [combo, _]: [Id; 2]| {248 let build = |ui: &mut Ui, [combo, _]: [Id; 2]| {
256 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());249 ui::popup::menu(ui, id, Anchor::Over(combo), &items, Some("Calibri"));
257 ui::popup::menu(ui, id, anchor, &items, Some("Calibri"));
258 };250 };
259 scene.open(id, Vec::new(), build);251 scene.open(id, Vec::new(), build);
260 let mut panels = vec![paint(device, queue, renderer, &scene.ui)];252 let mut panels = vec![paint(device, queue, renderer, &scene.ui)];
...@@ -543,10 +535,6 @@ impl Scene {...@@ -543,10 +535,6 @@ impl Scene {
543 }535 }
544}536}
545537
546fn below(ui: &Ui, id: Id) -> Anchor {
547 Anchor::Below(ui.rect(id).unwrap_or_default())
548}
549
550fn key(named: NamedKey) -> Event {538fn key(named: NamedKey) -> Event {
551 Event::Key {539 Event::Key {
552 key: Key::Named(named),540 key: Key::Named(named),
...@@ -656,8 +644,7 @@ fn fonts(scene: &mut Scene) {...@@ -656,8 +644,7 @@ fn fonts(scene: &mut Scene) {
656 let items = font_items();644 let items = font_items();
657 let id = Id::ROOT.child("fonts");645 let id = Id::ROOT.child("fonts");
658 scene.open(id, typed("ca"), |ui, [combo, _]| {646 scene.open(id, typed("ca"), |ui, [combo, _]| {
659 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());647 ui::popup::menu(ui, id, Anchor::Over(combo), &items, Some("Calibri"));
660 ui::popup::menu(ui, id, anchor, &items, Some("Calibri"));
661 });648 });
662}649}
663650
...@@ -707,16 +694,16 @@ fn colors(scene: &mut Scene) {...@@ -707,16 +694,16 @@ fn colors(scene: &mut Scene) {
707 let swatches = office_colors();694 let swatches = office_colors();
708 let id = Id::ROOT.child("font color");695 let id = Id::ROOT.child("font color");
709 scene.open(id, Vec::new(), |ui, [_, split]| {696 scene.open(id, Vec::new(), |ui, [_, split]| {
710 let anchor = below(ui, split);697 let anchor = Anchor::Below(split);
711 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);698 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);
712 });699 });
713 let cell = scene.ui.rect(id.child(("cell", 15_usize))).unwrap();700 let cell = scene.ui.laid_out(id.child(("cell", 15_usize))).unwrap();
714 scene701 scene
715 .ui702 .ui
716 .event(Event::PointerMoved([cell[0] + 5.0, cell[1] + 5.0]));703 .event(Event::PointerMoved([cell[0] + 5.0, cell[1] + 5.0]));
717 for _ in 0..30 {704 for _ in 0..30 {
718 scene.frame(|ui, [_, split]| {705 scene.frame(|ui, [_, split]| {
719 let anchor = below(ui, split);706 let anchor = Anchor::Below(split);
720 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);707 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);
721 });708 });
722 }709 }
...@@ -743,7 +730,7 @@ fn highlight(scene: &mut Scene) {...@@ -743,7 +730,7 @@ fn highlight(scene: &mut Scene) {
743 .map(|(r, g, b)| (draw::srgb(r, g, b), ""));730 .map(|(r, g, b)| (draw::srgb(r, g, b), ""));
744 let id = Id::ROOT.child("highlight");731 let id = Id::ROOT.child("highlight");
745 scene.open(id, vec![key(NamedKey::ArrowDown)], |ui, [_, split]| {732 scene.open(id, vec![key(NamedKey::ArrowDown)], |ui, [_, split]| {
746 let anchor = below(ui, split);733 let anchor = Anchor::Below(split);
747 ui::popup::colors(ui, id, anchor, "No Color", &swatches, 5);734 ui::popup::colors(ui, id, anchor, "No Color", &swatches, 5);
748 });735 });
749}736}
...@@ -820,21 +807,16 @@ fn tags(scene: &mut Scene) {...@@ -820,21 +807,16 @@ fn tags(scene: &mut Scene) {
820 ..Item::default()807 ..Item::default()
821 });808 });
822 let id = Id::ROOT.child("tags");809 let id = Id::ROOT.child("tags");
823 scene.open(id, Vec::new(), |ui, [combo, _]| {810 let build = |ui: &mut Ui, [_, split]: [Id; 2]| {
824 let rect = ui.rect(combo).unwrap_or_default();811 ui::popup::menu(ui, id, Anchor::Below(split), &tags, Some("Filter tags"));
825 let anchor = Anchor::Below([rect[0] + 200.0, rect[1], rect[0] + 222.0, rect[3]]);812 };
826 ui::popup::menu(ui, id, anchor, &tags, Some("Filter tags"));813 scene.open(id, Vec::new(), build);
827 });814 let row = scene.ui.laid_out(id.child("rows").child(3_usize)).unwrap();
828 let row = scene.ui.rect(id.child("rows").child(3_usize)).unwrap();
829 scene815 scene
830 .ui816 .ui
831 .event(Event::PointerMoved([row[0] + 40.0, row[1] + 8.0]));817 .event(Event::PointerMoved([row[0] + 40.0, row[1] + 8.0]));
832 for _ in 0..3 {818 for _ in 0..3 {
833 scene.frame(|ui, [combo, _]| {819 scene.frame(build);
834 let rect = ui.rect(combo).unwrap_or_default();
835 let anchor = Anchor::Below([rect[0] + 200.0, rect[1], rect[0] + 222.0, rect[3]]);
836 ui::popup::menu(ui, id, anchor, &tags, Some("Filter tags"));
837 });
838 }820 }
839}821}
840822
crates/ui/src/access/tests.rs+6-7
...@@ -187,14 +187,13 @@ fn toolbar(ui: &mut Ui, bold: bool) -> Vec<&'static str> {...@@ -187,14 +187,13 @@ fn toolbar(ui: &mut Ui, bold: bool) -> Vec<&'static str> {
187 ..popup::Item::default()187 ..popup::Item::default()
188 },188 },
189 ];189 ];
190 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());190 popup::menu(ui, font_menu(), Anchor::Over(combo), &items, Some("Font"));
191 popup::menu(ui, font_menu(), anchor, &items, Some("Font"));
192 let paste = [popup::Item {191 let paste = [popup::Item {
193 text: "Keep Text Only",192 text: "Keep Text Only",
194 shortcut: "⌘⇧V",193 shortcut: "⌘⇧V",
195 ..popup::Item::default()194 ..popup::Item::default()
196 }];195 }];
197 let anchor = Anchor::Below(ui.rect(ui.id("paste")).unwrap_or_default());196 let anchor = Anchor::Below(ui.id("paste"));
198 popup::menu(ui, more_menu(), anchor, &paste, None);197 popup::menu(ui, more_menu(), anchor, &paste, None);
199 clicked198 clicked
200}199}
...@@ -256,7 +255,7 @@ fn a_disabled_combo_shows_its_value_but_takes_no_clicks_or_focus() {...@@ -256,7 +255,7 @@ fn a_disabled_combo_shows_its_value_but_takes_no_clicks_or_focus() {
256 });255 });
257 };256 };
258 build(&mut ui);257 build(&mut ui);
259 let [left, top, right, bottom] = ui.rect(ui.id("font")).unwrap();258 let [left, top, right, bottom] = ui.laid_out(ui.id("font")).unwrap();
260 let middle = [(left + right) / 2.0, (top + bottom) / 2.0];259 let middle = [(left + right) / 2.0, (top + bottom) / 2.0];
261 ui.event(Event::PointerMoved(middle));260 ui.event(Event::PointerMoved(middle));
262 for pressed in [true, false] {261 for pressed in [true, false] {
...@@ -317,7 +316,7 @@ fn a_row_opening_a_menu_says_so_instead_of_naming_keys() {...@@ -317,7 +316,7 @@ fn a_row_opening_a_menu_says_so_instead_of_naming_keys() {
317 ui.open_popup(menu);316 ui.open_popup(menu);
318 for _ in 0..2 {317 for _ in 0..2 {
319 frame(&mut ui, |ui| {318 frame(&mut ui, |ui| {
320 popup::menu(ui, menu, Anchor::Below([0.0; 4]), &items, None);319 popup::menu(ui, menu, Anchor::Point([0.0; 2]), &items, None);
321 });320 });
322 }321 }
323 let tree = tree(&mut ui);322 let tree = tree(&mut ui);
...@@ -348,7 +347,7 @@ fn swatches_are_named_by_their_colour_names_or_hex() {...@@ -348,7 +347,7 @@ fn swatches_are_named_by_their_colour_names_or_hex() {
348 ui.open_popup(grid);347 ui.open_popup(grid);
349 for _ in 0..2 {348 for _ in 0..2 {
350 frame(&mut ui, |ui| {349 frame(&mut ui, |ui| {
351 popup::colors(ui, grid, Anchor::Below([0.0; 4]), "No Color", &swatches, 2);350 popup::colors(ui, grid, Anchor::Point([0.0; 2]), "No Color", &swatches, 2);
352 });351 });
353 }352 }
354 let snapshot = snapshot(&tree(&mut ui));353 let snapshot = snapshot(&tree(&mut ui));
...@@ -782,7 +781,7 @@ fn a_segmented_control_is_a_radio_group_tab_enters_at_its_choice_and_arrows_pick...@@ -782,7 +781,7 @@ fn a_segmented_control_is_a_radio_group_tab_enters_at_its_choice_and_arrows_pick
782 }781 }
783 assert_eq!((ui.focused(), chosen), (segment(0), 0), "an arrow picks");782 assert_eq!((ui.focused(), chosen), (segment(0), 0), "an arrow picks");
784 // A click picks without moving the focus, which stays put on later frames.783 // A click picks without moving the focus, which stays put on later frames.
785 let [left, top, right, bottom] = ui.rect(segment(1).unwrap()).unwrap();784 let [left, top, right, bottom] = ui.laid_out(segment(1).unwrap()).unwrap();
786 ui.event(Event::PointerMoved([785 ui.event(Event::PointerMoved([
787 (left + right) / 2.0,786 (left + right) / 2.0,
788 (top + bottom) / 2.0,787 (top + bottom) / 2.0,
crates/ui/src/layout.rs+128-36
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1use crate::{1use crate::{
2 Anchor, Axis, Built, Flags, ICON, ICON_GAP, Id, Overflow, Size, State, fitting, text::Texts,2 Anchor, Axis, Built, Flags, ICON, ICON_GAP, Id, Overflow, Size, State, fitting, popup::PAD,
3 text::Texts,
3};4};
4use std::collections::HashMap;5use std::collections::HashMap;
56
...@@ -7,8 +8,9 @@ use std::collections::HashMap;...@@ -7,8 +8,9 @@ use std::collections::HashMap;
7/// ancestors (pre-order), sizes summed from children (post-order), overflow taken back8/// ancestors (pre-order), sizes summed from children (post-order), overflow taken back
8/// (by folding a row's groups for what space sized by ancestors can't give, then from that9/// (by folding a row's groups for what space sized by ancestors can't give, then from that
9/// space, then from the least strict boxes first), and positions along each parent's flow. Labels too wide for their10/// space, then from the least strict boxes first), and positions along each parent's flow. Labels too wide for their
10/// solved width wrap or shorten before heights are solved. Boxes are in build order, so11/// solved width wrap or shorten before heights are solved. Boxes are in build order, so a
11/// index order is pre-order.12/// parent comes before its children. The interface is solved first, then each popup in
13/// turn beside its anchor's box as laid out by then, or as last laid out where not yet.
12pub(crate) fn solve(14pub(crate) fn solve(
13 nodes: &mut [Built],15 nodes: &mut [Built],
14 states: &HashMap<Id, State>,16 states: &HashMap<Id, State>,
...@@ -22,15 +24,97 @@ pub(crate) fn solve(...@@ -22,15 +24,97 @@ pub(crate) fn solve(
22 nodes[folded].hidden = true;24 nodes[folded].hidden = true;
23 }25 }
24 }26 }
27 // The popup each box lies in, by the popup's index; 0 for the interface beneath.
28 let mut popup = vec![0; nodes.len()];
29 for index in 1..nodes.len() {
30 popup[index] = match nodes[index].anchor {
31 Some(_) => index,
32 None => popup[nodes[index].parent],
33 };
34 }
35 let members = |group: usize| -> Vec<usize> {
36 (0..popup.len())
37 .filter(|index| popup[*index] == group)
38 .collect()
39 };
40 solve_group(nodes, &members(0), states, scale, texts, frame);
41 for group in 1..nodes.len() {
42 if popup[group] == group {
43 resolve(nodes, &popup, group, states);
44 solve_group(nodes, &members(group), states, scale, texts, frame);
45 }
46 }
47}
48
49/// Finds where popup `index` opens from its anchor's box, laid out before it in `popup`'s
50/// order, or as last laid out, and what it takes from the box: a popup below or over it is
51/// at least as wide, and what holds still in a popup over it stands in for it as tall.
52fn resolve(nodes: &mut [Built], popup: &[usize], index: usize, states: &HashMap<Id, State>) {
53 let node = &nodes[index];
54 let anchor = node.anchor.expect("a popup has an anchor");
55 let laid_out = |id: Id| {
56 let found = (1..nodes.len()).find(|at| nodes[*at].id == id && popup[*at] < index);
57 found
58 .map(|at| (nodes[at].rect, popup[at]))
59 .or_else(|| Some((states.get(&id)?.rect?, 0)))
60 };
61 let [pad_x, pad_y] = node.pad;
62 let around = match anchor {
63 Anchor::Point([x, y]) => [x, y, x, y],
64 Anchor::Dialog | Anchor::Top => [0.0; 4],
65 Anchor::Below(id) | Anchor::Right(id) | Anchor::Over(id) => {
66 let (rect, holder) = laid_out(id).unwrap_or_default();
67 let [left, top, right, bottom] = rect;
68 match anchor {
69 Anchor::Right(_) => {
70 let [left, _, right, _] = if holder == 0 {
71 rect
72 } else {
73 nodes[holder].rect
74 };
75 [left, top - pad_y, right, bottom + pad_y]
76 }
77 Anchor::Over(_) => [left - pad_x, top - pad_y, right + pad_x, bottom + pad_y],
78 _ => [left, top - PAD, right, bottom + PAD],
79 }
80 }
81 };
82 let node = &mut nodes[index];
83 node.around = around;
84 if let (Some(Anchor::Below(_) | Anchor::Over(_)), Size::Pixels(width)) =
85 (node.anchor, node.size[0].size)
86 {
87 node.size[0].size = Size::Pixels(width.max(around[2] - around[0]));
88 }
89 if let Anchor::Over(_) = anchor {
90 let height = around[3] - around[1] - 2.0 * pad_y;
91 for at in index..nodes.len() {
92 if popup[at] == index && nodes[at].flags.contains(Flags::STILL) {
93 nodes[at].size[1].size = Size::Pixels(height);
94 }
95 }
96 }
97}
98
99/// Solves the boxes `members`, the interface or a popup and what it holds, in build order.
100fn solve_group(
101 nodes: &mut [Built],
102 members: &[usize],
103 states: &HashMap<Id, State>,
104 scale: f32,
105 texts: &mut Texts,
106 frame: u64,
107) {
25 for axis in 0..2 {108 for axis in 0..2 {
26 if axis == 1 {109 if axis == 1 {
27 fit_labels(nodes, texts, frame);110 fit_labels(nodes, members, texts, frame);
28 }111 }
29 for node in nodes.iter_mut() {112 for &index in members {
113 let node = &mut nodes[index];
30 node.computed[axis] = match node.size[axis].size {114 node.computed[axis] = match node.size[axis].size {
31 Size::Pixels(pixels) => match node.anchor {115 Size::Pixels(pixels) => match node.anchor {
32 Some(Anchor::Over(rect)) if axis == 0 => {116 Some(Anchor::Over(_)) if axis == 0 => {
33 let from = rect[2] - rect[0];117 let from = node.around[2] - node.around[0];
34 from + (pixels - from) * node.open118 from + (pixels - from) * node.open
35 }119 }
36 _ => pixels,120 _ => pixels,
...@@ -51,8 +135,8 @@ pub(crate) fn solve(...@@ -51,8 +135,8 @@ pub(crate) fn solve(
51 Size::Fraction(_) | Size::Children => 0.0,135 Size::Fraction(_) | Size::Children => 0.0,
52 };136 };
53 }137 }
54 fit_popups(nodes, axis);138 fit_popups(nodes, members, axis);
55 for index in 1..nodes.len() {139 for &index in members.iter().filter(|index| **index != 0) {
56 if let Size::Fraction(fraction) = nodes[index].size[axis].size {140 if let Size::Fraction(fraction) = nodes[index].size[axis].size {
57 if stretches(nodes, index, axis) {141 if stretches(nodes, index, axis) {
58 nodes[index].computed[axis] = 0.0;142 nodes[index].computed[axis] = 0.0;
...@@ -66,15 +150,15 @@ pub(crate) fn solve(...@@ -66,15 +150,15 @@ pub(crate) fn solve(
66 nodes[index].computed[axis] = room.max(0.0) * fraction;150 nodes[index].computed[axis] = room.max(0.0) * fraction;
67 }151 }
68 }152 }
69 for index in (0..nodes.len()).rev() {153 for &index in members.iter().rev() {
70 if nodes[index].size[axis].size == Size::Children {154 if nodes[index].size[axis].size == Size::Children {
71 let content = flow(nodes, index, axis);155 let content = flow(nodes, index, axis);
72 nodes[index].computed[axis] = content + 2.0 * nodes[index].pad[axis];156 nodes[index].computed[axis] = content + 2.0 * nodes[index].pad[axis];
73 }157 }
74 }158 }
75 yield_popups(nodes, axis);159 yield_popups(nodes, members, axis);
76 fit_popups(nodes, axis);160 fit_popups(nodes, members, axis);
77 for index in 0..nodes.len() {161 for &index in members {
78 let node = &nodes[index];162 let node = &nodes[index];
79 if node.children.is_empty() || (axis == 1 && node.flags.contains(Flags::SCROLL)) {163 if node.children.is_empty() || (axis == 1 && node.flags.contains(Flags::SCROLL)) {
80 continue;164 continue;
...@@ -144,24 +228,30 @@ pub(crate) fn solve(...@@ -144,24 +228,30 @@ pub(crate) fn solve(
144 }228 }
145 }229 }
146 let window = nodes[0].computed[axis];230 let window = nodes[0].computed[axis];
147 for index in 0..nodes.len() {231 for &index in members {
232 let node = &nodes[index];
233 if let Some(anchor) = node.anchor {
234 let shown = node.computed[axis];
235 let size = match node.size[axis].size {
236 Size::Pixels(pixels) => pixels,
237 _ => shown,
238 };
239 nodes[index].relative[axis] = anchor.place(node.around, axis, size, shown, window);
240 }
148 let mut cursor = nodes[index].pad[axis];241 let mut cursor = nodes[index].pad[axis];
149 // Where a popup widening over its anchor lays its children out, from where it is.242 // Where a popup widening over its anchor lays its children out, from where it is.
150 let shift = match (nodes[index].anchor, nodes[index].size[axis].size) {243 let shift = match (nodes[index].anchor, nodes[index].size[axis].size) {
151 (Some(anchor @ Anchor::Over(_)), Size::Pixels(full)) if axis == 0 => {244 (Some(anchor @ Anchor::Over(_)), Size::Pixels(full)) if axis == 0 => {
152 anchor.place(axis, full, full, window) - nodes[index].relative[axis]245 let around = nodes[index].around;
246 anchor.place(around, axis, full, full, window) - nodes[index].relative[axis]
153 }247 }
154 _ => 0.0,248 _ => 0.0,
155 };249 };
156 for child in nodes[index].children.clone() {250 for child in nodes[index].children.clone() {
157 nodes[child].relative[axis] = if let Some(anchor) = nodes[child].anchor {251 if nodes[child].anchor.is_some() {
158 let shown = nodes[child].computed[axis];252 continue;
159 let size = match nodes[child].size[axis].size {253 }
160 Size::Pixels(pixels) => pixels,254 nodes[child].relative[axis] = if nodes[child].flags.contains(Flags::FLOAT) {
161 _ => shown,
162 };
163 anchor.place(axis, size, shown, window)
164 } else if nodes[child].flags.contains(Flags::FLOAT) {
165 nodes[child].position[axis]255 nodes[child].position[axis]
166 } else if nodes[child].hidden {256 } else if nodes[child].hidden {
167 nodes[index].pad[axis]257 nodes[index].pad[axis]
...@@ -182,13 +272,12 @@ pub(crate) fn solve(...@@ -182,13 +272,12 @@ pub(crate) fn solve(
182 }272 }
183 }273 }
184 let snap = |value: f32| (value * scale).round() / scale;274 let snap = |value: f32| (value * scale).round() / scale;
185 nodes[0].rect = [275 for &index in members {
186 0.0,276 if index == 0 {
187 0.0,277 let [width, height] = nodes[0].computed;
188 snap(nodes[0].computed[0]),278 nodes[0].rect = [0.0, 0.0, snap(width), snap(height)];
189 snap(nodes[0].computed[1]),279 continue;
190 ];280 }
191 for index in 1..nodes.len() {
192 let parent = nodes[index].parent;281 let parent = nodes[index].parent;
193 if nodes[index].hidden || nodes[parent].hidden {282 if nodes[index].hidden || nodes[parent].hidden {
194 nodes[index].hidden = true;283 nodes[index].hidden = true;
...@@ -229,9 +318,10 @@ fn stretches(nodes: &[Built], index: usize, axis: usize) -> bool {...@@ -229,9 +318,10 @@ fn stretches(nodes: &[Built], index: usize, axis: usize) -> bool {
229318
230/// Lets each popup sized loosely give way to the window, as far as its strictness lets it:319/// Lets each popup sized loosely give way to the window, as far as its strictness lets it:
231/// a dialog keeps below it the margin it opens under.320/// a dialog keeps below it the margin it opens under.
232fn yield_popups(nodes: &mut [Built], axis: usize) {321fn yield_popups(nodes: &mut [Built], members: &[usize], axis: usize) {
233 let window = nodes[0].computed[axis];322 let window = nodes[0].computed[axis];
234 for node in &mut nodes[1..] {323 for &index in members {
324 let node = &mut nodes[index];
235 let room = match node.anchor {325 let room = match node.anchor {
236 None => continue,326 None => continue,
237 Some(Anchor::Dialog | Anchor::Top) if axis == 1 => window * 3.0 / 4.0,327 Some(Anchor::Dialog | Anchor::Top) if axis == 1 => window * 3.0 / 4.0,
...@@ -246,9 +336,10 @@ fn yield_popups(nodes: &mut [Built], axis: usize) {...@@ -246,9 +336,10 @@ fn yield_popups(nodes: &mut [Built], axis: usize) {
246336
247/// Shrinks each popup longer than the window lets it be on `axis`; one cut short vertically337/// Shrinks each popup longer than the window lets it be on `axis`; one cut short vertically
248/// scrolls what it holds.338/// scrolls what it holds.
249fn fit_popups(nodes: &mut [Built], axis: usize) {339fn fit_popups(nodes: &mut [Built], members: &[usize], axis: usize) {
250 let most = fitting(nodes[0].computed[axis]);340 let most = fitting(nodes[0].computed[axis]);
251 for node in &mut nodes[1..] {341 for &index in members {
342 let node = &mut nodes[index];
252 if node.anchor.is_some() && node.computed[axis] > most {343 if node.anchor.is_some() && node.computed[axis] > most {
253 node.computed[axis] = most;344 node.computed[axis] = most;
254 if axis == 1 {345 if axis == 1 {
...@@ -374,8 +465,9 @@ fn least(nodes: &[Built], index: usize) -> f32 {...@@ -374,8 +465,9 @@ fn least(nodes: &[Built], index: usize) -> f32 {
374 own * node.size[0].strictness.clamp(0.0, 1.0)465 own * node.size[0].strictness.clamp(0.0, 1.0)
375}466}
376467
377fn fit_labels(nodes: &mut [Built], texts: &mut Texts, frame: u64) {468fn fit_labels(nodes: &mut [Built], members: &[usize], texts: &mut Texts, frame: u64) {
378 for node in nodes.iter_mut() {469 for &index in members {
470 let node = &mut nodes[index];
379 let Some(label) = node.label.clone() else {471 let Some(label) = node.label.clone() else {
380 continue;472 continue;
381 };473 };
crates/ui/src/lib.rs+125-91
...@@ -167,7 +167,7 @@ impl Flags {...@@ -167,7 +167,7 @@ impl Flags {
167 /// The host paints it and receives the events routed to it.167 /// The host paints it and receives the events routed to it.
168 pub const CUSTOM: Flags = Flags(32);168 pub const CUSTOM: Flags = Flags(32);
169 /// Paints in place while the popup around it opens, as a field standing in for the one169 /// Paints in place while the popup around it opens, as a field standing in for the one
170 /// it opened from.170 /// it opened from, and as tall as that box.
171 pub const STILL: Flags = Flags(64);171 pub const STILL: Flags = Flags(64);
172172
173 pub(crate) fn contains(self, other: Flags) -> bool {173 pub(crate) fn contains(self, other: Flags) -> bool {
...@@ -219,14 +219,17 @@ fn fitting(room: f32) -> f32 {...@@ -219,14 +219,17 @@ fn fitting(room: f32) -> f32 {
219}219}
220220
221/// Where a popup opens, flipping to the far side of its anchor where the window ends first.221/// Where a popup opens, flipping to the far side of its anchor where the window ends first.
222/// The box it opens beside is placed where layout puts it in the same frame.
222#[derive(Clone, Copy, Debug, PartialEq)]223#[derive(Clone, Copy, Debug, PartialEq)]
223pub enum Anchor {224pub enum Anchor {
224 /// Under the rectangle, from its leading edge, as a drop-down opens.225 /// Under the box, from its leading edge, as a drop-down opens; at least as wide as it.
225 Below([f32; 4]),226 Below(Id),
226 /// Past the rectangle's trailing edge, from its top, as a submenu opens.227 /// Past the trailing edge of the box, or of the popup holding it, level with the box's
227 Right([f32; 4]),228 /// top, as a submenu opens.
228 /// Over the rectangle from its corner, as a combo box opens into its own list.229 Right(Id),
229 Over([f32; 4]),230 /// Over the box from its corner, the popup's contents covering it, as a combo box opens
231 /// into its own list; at least as wide as it.
232 Over(Id),
230 /// At a point, as a context menu opens.233 /// At a point, as a context menu opens.
231 Point([f32; 2]),234 Point([f32; 2]),
232 /// Centred across the window near its top, over the interface, which dims, as a dialog235 /// Centred across the window near its top, over the interface, which dims, as a dialog
...@@ -238,24 +241,23 @@ pub enum Anchor {...@@ -238,24 +241,23 @@ pub enum Anchor {
238}241}
239242
240impl Anchor {243impl Anchor {
241 /// Where a popup `size` long on `axis` starts in a window `room` long: past the244 /// Where a popup `size` long on `axis` starts in a window `room` long: past `around`, the
242 /// anchor on the axis it opens along, level with it otherwise. Shown only `shown` long245 /// rectangle it opens beside, on the axis it opens along, level with it otherwise. Shown
243 /// as it opens, it keeps the edge it would have at full size.246 /// only `shown` long as it opens, it keeps the edge it would have at full size.
244 fn place(self, axis: usize, size: f32, shown: f32, room: f32) -> f32 {247 fn place(self, around: [f32; 4], axis: usize, size: f32, shown: f32, room: f32) -> f32 {
245 let size = size.min(fitting(room));248 let size = size.min(fitting(room));
246 let shown = shown.min(size);249 let shown = shown.min(size);
247 let most = (room - POPUP_MARGIN - size).max(POPUP_MARGIN);250 let most = (room - POPUP_MARGIN - size).max(POPUP_MARGIN);
248 let (rect, along) = match self {251 let along = match self {
249 Anchor::Below(rect) => (rect, Some(1)),252 Anchor::Below(_) | Anchor::Point(_) => Some(1),
250 Anchor::Right(rect) => (rect, Some(0)),253 Anchor::Right(_) => Some(0),
251 Anchor::Over(rect) => (rect, None),254 Anchor::Over(_) => None,
252 Anchor::Point([x, y]) => ([x, y, x, y], Some(1)),
253 Anchor::Dialog | Anchor::Top if axis == 0 => {255 Anchor::Dialog | Anchor::Top if axis == 0 => {
254 return ((room - size) / 2.0).max(POPUP_MARGIN);256 return ((room - size) / 2.0).max(POPUP_MARGIN);
255 }257 }
256 Anchor::Dialog | Anchor::Top => return (room / 8.0).clamp(POPUP_MARGIN, most),258 Anchor::Dialog | Anchor::Top => return (room / 8.0).clamp(POPUP_MARGIN, most),
257 };259 };
258 let [low, high] = [rect[axis], rect[axis + 2]];260 let [low, high] = [around[axis], around[axis + 2]];
259 let (first, second) = if along == Some(axis) {261 let (first, second) = if along == Some(axis) {
260 (high, low - size)262 (high, low - size)
261 } else {263 } else {
...@@ -270,42 +272,6 @@ impl Anchor {...@@ -270,42 +272,6 @@ impl Anchor {
270 }272 }
271 }273 }
272274
273 /// How a popup laid out at `rect` beside the anchor shows `open` of the way open: where it
274 /// `grows`, it swings out of the anchor's edge as it grows and fades in, and a dialog swings
275 /// up into place.
276 fn motion(self, rect: [f32; 4], open: f32, grows: bool) -> Motion {
277 let pivot = match self {
278 Anchor::Below(anchor) => [anchor[0], anchor[3]],
279 Anchor::Right(anchor) => [anchor[2], anchor[1]],
280 Anchor::Over(anchor) => [anchor[0], anchor[1]],
281 Anchor::Point(point) => point,
282 Anchor::Dialog | Anchor::Top => [(rect[0] + rect[2]) / 2.0, rect[1]],
283 };
284 let (from, tilt) = match self {
285 _ if !grows => (1.0, 0.0),
286 Anchor::Dialog | Anchor::Top => (0.95, 0.2),
287 // Over a box, the popup widens out of it in layout instead of growing.
288 Anchor::Over(_) => (1.0, 0.2),
289 _ => (0.94, 0.2),
290 };
291 let pivot = [
292 pivot[0].clamp(rect[0], rect[2]),
293 pivot[1].clamp(rect[1], rect[3]),
294 ];
295 // A popup flipped above its anchor swings out of its bottom edge, its top leaning away.
296 let tilt = if pivot[1] > (rect[1] + rect[3]) / 2.0 {
297 -tilt
298 } else {
299 tilt
300 };
301 Motion {
302 zoom: from + (1.0 - from) * open,
303 pivot,
304 tilt: tilt * (1.0 - open),
305 opacity: open,
306 }
307 }
308
309 /// Seconds the popup takes to open and to close.275 /// Seconds the popup takes to open and to close.
310 fn durations(self) -> [f32; 2] {276 fn durations(self) -> [f32; 2] {
311 if matches!(self, Anchor::Dialog | Anchor::Top) {277 if matches!(self, Anchor::Dialog | Anchor::Top) {
...@@ -534,6 +500,8 @@ struct Built {...@@ -534,6 +500,8 @@ struct Built {
534 offset: [f32; 2],500 offset: [f32; 2],
535 cursor: Option<CursorIcon>,501 cursor: Option<CursorIcon>,
536 anchor: Option<Anchor>,502 anchor: Option<Anchor>,
503 /// The rectangle a popup opens beside, as layout resolves its anchor.
504 around: [f32; 4],
537 /// How far open a popup over a box is, as it widens out of the box.505 /// How far open a popup over a box is, as it widens out of the box.
538 open: f32,506 open: f32,
539 motion: Option<Motion>,507 motion: Option<Motion>,
...@@ -604,12 +572,61 @@ struct Tip {...@@ -604,12 +572,61 @@ struct Tip {
604 due: Option<Instant>,572 due: Option<Instant>,
605}573}
606574
607/// A popup's painting as it last showed, fading out since it closed.575/// A popup painted: its anchor, the rectangle it opens beside, and its own.
608struct Closing {576#[derive(Clone, Copy)]
577struct Placed {
609 id: Id,578 id: Id,
610 display: Vec<Display>,
611 anchor: Anchor,579 anchor: Anchor,
580 around: [f32; 4],
612 rect: [f32; 4],581 rect: [f32; 4],
582}
583
584impl Placed {
585 /// How the popup shows `open` of the way open: where it `grows`, it swings out of its
586 /// anchor's edge as it grows and fades in, and a dialog swings up into place.
587 fn motion(self, open: f32, grows: bool) -> Motion {
588 let Placed {
589 anchor,
590 around,
591 rect,
592 ..
593 } = self;
594 let pivot = match anchor {
595 Anchor::Below(_) | Anchor::Point(_) => [around[0], around[3]],
596 Anchor::Right(_) => [around[2], around[1]],
597 Anchor::Over(_) => [around[0], around[1]],
598 Anchor::Dialog | Anchor::Top => [(rect[0] + rect[2]) / 2.0, rect[1]],
599 };
600 let (from, tilt) = match anchor {
601 _ if !grows => (1.0, 0.0),
602 Anchor::Dialog | Anchor::Top => (0.95, 0.2),
603 // Over a box, the popup widens out of it in layout instead of growing.
604 Anchor::Over(_) => (1.0, 0.2),
605 _ => (0.94, 0.2),
606 };
607 let pivot = [
608 pivot[0].clamp(rect[0], rect[2]),
609 pivot[1].clamp(rect[1], rect[3]),
610 ];
611 // A popup flipped above its anchor swings out of its bottom edge, its top leaning away.
612 let tilt = if pivot[1] > (rect[1] + rect[3]) / 2.0 {
613 -tilt
614 } else {
615 tilt
616 };
617 Motion {
618 zoom: from + (1.0 - from) * open,
619 pivot,
620 tilt: tilt * (1.0 - open),
621 opacity: open,
622 }
623 }
624}
625
626/// A popup's painting as it last showed, fading out since it closed.
627struct Closing {
628 placed: Placed,
629 display: Vec<Display>,
613 closed: Instant,630 closed: Instant,
614}631}
615632
...@@ -716,9 +733,8 @@ pub struct Ui {...@@ -716,9 +733,8 @@ pub struct Ui {
716 still: Vec<Display>,733 still: Vec<Display>,
717 /// Where the popups' painting starts in `display`, for edges drawn beneath them.734 /// Where the popups' painting starts in `display`, for edges drawn beneath them.
718 popups_painted: usize,735 popups_painted: usize,
719 /// Each popup painted, its anchor and rectangle, and its painting's place after736 /// Each popup painted, and its painting's place after `popups_painted`.
720 /// `popups_painted`.737 painted: Vec<(Placed, std::ops::Range<usize>)>,
721 painted: Vec<(Id, Anchor, [f32; 4], std::ops::Range<usize>)>,
722 closing: Vec<Closing>,738 closing: Vec<Closing>,
723 tip: Option<Tip>,739 tip: Option<Tip>,
724 /// When a tooltip last showed.740 /// When a tooltip last showed.
...@@ -739,6 +755,8 @@ pub struct Ui {...@@ -739,6 +755,8 @@ pub struct Ui {
739 resume: Option<Id>,755 resume: Option<Id>,
740 /// The nodes last sent to assistive technology.756 /// The nodes last sent to assistive technology.
741 sent: HashMap<Id, accesskit::Node>,757 sent: HashMap<Id, accesskit::Node>,
758 /// Scrolling boxes to scroll to a box within each, once laid out.
759 scrolls: Vec<(Id, Id)>,
742}760}
743761
744impl Ui {762impl Ui {
...@@ -786,6 +804,7 @@ impl Ui {...@@ -786,6 +804,7 @@ impl Ui {
786 focus_ring: false,804 focus_ring: false,
787 resume: None,805 resume: None,
788 sent: HashMap::new(),806 sent: HashMap::new(),
807 scrolls: Vec::new(),
789 }808 }
790 }809 }
791810
...@@ -867,21 +886,27 @@ impl Ui {...@@ -867,21 +886,27 @@ impl Ui {
867 Some(layout::narrowest(&self.nodes, index))886 Some(layout::narrowest(&self.nodes, index))
868 }887 }
869888
870 /// The box's rectangle from the latest layout; none for one not laid out yet.889 /// Where box `id` lies in the latest layout: this frame's once `end` has run, otherwise
871 pub fn rect(&self, id: Id) -> Option<[f32; 4]> {890 /// the previous frame's, which input is routed against; none for one not laid out yet.
891 /// For hosts painting, hit-testing or mapping input after layout: a box placed beside
892 /// another takes an `Anchor`, which layout resolves in the same frame.
893 pub fn laid_out(&self, id: Id) -> Option<[f32; 4]> {
872 self.states.get(&id).and_then(|state| state.rect)894 self.states.get(&id).and_then(|state| state.rect)
873 }895 }
874896
875 /// Scrolls `scroller`, a scrolling box, until `target` within it starts at its top, both897 /// The window's size in logical pixels this frame.
876 /// as last laid out, easing there.898 pub fn size(&self) -> [f32; 2] {
899 let root = self.nodes.first().map(|root| root.size);
900 root.unwrap_or_default().map(|extent| match extent.size {
901 Size::Pixels(pixels) => pixels,
902 _ => 0.0,
903 })
904 }
905
906 /// Scrolls `scroller`, a scrolling box, until `target` within it starts at its top, as
907 /// this frame lays them out, easing there.
877 pub fn scroll_to(&mut self, scroller: Id, target: Id) {908 pub fn scroll_to(&mut self, scroller: Id, target: Id) {
878 let (Some(top), Some(start)) = (self.rect(target), self.rect(scroller)) else {909 self.scrolls.push((scroller, target));
879 return;
880 };
881 let state = self.states.entry(scroller).or_default();
882 let most = (state.content - (start[3] - start[1])).max(0.0);
883 state.scroll_target = (state.scroll + top[1] - start[1]).clamp(0.0, most);
884 self.animating |= state.scroll != state.scroll_target;
885 }910 }
886911
887 /// The pointer's cursor, or None over a custom box, whose host chooses.912 /// The pointer's cursor, or None over a custom box, whose host chooses.
...@@ -981,7 +1006,7 @@ impl Ui {...@@ -981,7 +1006,7 @@ impl Ui {
981 self.focus_ring = false;1006 self.focus_ring = false;
982 if !self.popups.is_empty() {1007 if !self.popups.is_empty() {
983 let under = self.popups.iter().rposition(|popup| {1008 let under = self.popups.iter().rposition(|popup| {
984 self.rect(popup.id)1009 self.laid_out(popup.id)
985 .is_some_and(|rect| contains(rect, point))1010 .is_some_and(|rect| contains(rect, point))
986 });1011 });
987 // A press outside every popup only dismisses them.1012 // A press outside every popup only dismisses them.
...@@ -1385,6 +1410,15 @@ impl Ui {...@@ -1385,6 +1410,15 @@ impl Ui {
1385 state.scroll = state.scroll.clamp(0.0, most);1410 state.scroll = state.scroll.clamp(0.0, most);
1386 }1411 }
1387 }1412 }
1413 for (scroller, target) in std::mem::take(&mut self.scrolls) {
1414 let (Some(top), Some(start)) = (self.laid_out(target), self.laid_out(scroller)) else {
1415 continue;
1416 };
1417 let state = self.states.entry(scroller).or_default();
1418 let most = (state.content - (start[3] - start[1])).max(0.0);
1419 state.scroll_target = (state.scroll + top[1] - start[1]).clamp(0.0, most);
1420 self.animating |= state.scroll != state.scroll_target;
1421 }
1388 self.states1422 self.states
1389 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);1423 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);
1390 self.texts.prune(self.frame);1424 self.texts.prune(self.frame);
...@@ -1415,14 +1449,19 @@ impl Ui {...@@ -1415,14 +1449,19 @@ impl Ui {
1415 let Some(anchor) = node.anchor else {1449 let Some(anchor) = node.anchor else {
1416 continue;1450 continue;
1417 };1451 };
1418 let (id, rect) = (node.id, node.rect);1452 let placed = Placed {
1419 if Some(id) == tip {1453 id: node.id,
1454 anchor,
1455 around: node.around,
1456 rect: node.rect,
1457 };
1458 if Some(placed.id) == tip {
1420 continue;1459 continue;
1421 }1460 }
1422 if let Some(open) = self.opening(id, anchor) {1461 if let Some(open) = self.opening(placed.id, anchor) {
1423 self.scrim(anchor, open);1462 self.scrim(anchor, open);
1424 let grows = self.popup_motion(anchor) == PopupMotion::Grow;1463 let grows = self.popup_motion(anchor) == PopupMotion::Grow;
1425 self.nodes[index].motion = Some(anchor.motion(rect, open, grows));1464 self.nodes[index].motion = Some(placed.motion(open, grows));
1426 }1465 }
1427 let from = self.display.len() - self.popups_painted;1466 let from = self.display.len() - self.popups_painted;
1428 self.paint(index, None, None);1467 self.paint(index, None, None);
...@@ -1435,15 +1474,10 @@ impl Ui {...@@ -1435,15 +1474,10 @@ impl Ui {
1435 Display::Round(None),1474 Display::Round(None),
1436 ]);1475 ]);
1437 }1476 }
1438 painted.push((1477 painted.push((placed, from..self.display.len() - self.popups_painted));
1439 id,
1440 anchor,
1441 rect,
1442 from..self.display.len() - self.popups_painted,
1443 ));
1444 }1478 }
1445 for (id, anchor, rect, range) in std::mem::replace(&mut self.painted, painted) {1479 for (placed, range) in std::mem::replace(&mut self.painted, painted) {
1446 if self.painted.iter().any(|(shown, ..)| *shown == id) {1480 if self.painted.iter().any(|(shown, _)| shown.id == placed.id) {
1447 continue;1481 continue;
1448 }1482 }
1449 // What held still while it opened goes at once, uncovering what it stood in for.1483 // What held still while it opened goes at once, uncovering what it stood in for.
...@@ -1460,18 +1494,17 @@ impl Ui {...@@ -1460,18 +1494,17 @@ impl Ui {
1460 .cloned()1494 .cloned()
1461 .collect();1495 .collect();
1462 self.closing.push(Closing {1496 self.closing.push(Closing {
1463 id,1497 placed,
1464 display,1498 display,
1465 anchor,
1466 rect,
1467 closed: self.now,1499 closed: self.now,
1468 });1500 });
1469 }1501 }
1470 for closing in std::mem::take(&mut self.closing) {1502 for closing in std::mem::take(&mut self.closing) {
1471 let motion = self.popup_motion(closing.anchor);1503 let Placed { id, anchor, .. } = closing.placed;
1504 let motion = self.popup_motion(anchor);
1472 let open = match motion {1505 let open = match motion {
1473 PopupMotion::Grow => {1506 PopupMotion::Grow => {
1474 (1.0 - self.progress(closing.closed, closing.anchor.durations()[1])).powi(3)1507 (1.0 - self.progress(closing.closed, anchor.durations()[1])).powi(3)
1475 }1508 }
1476 PopupMotion::Cut => 0.0,1509 PopupMotion::Cut => 0.0,
1477 PopupMotion::Fade([_, close]) => {1510 PopupMotion::Fade([_, close]) => {
...@@ -1481,13 +1514,13 @@ impl Ui {...@@ -1481,13 +1514,13 @@ impl Ui {
1481 if open == 0.0 {1514 if open == 0.0 {
1482 continue;1515 continue;
1483 }1516 }
1484 self.scrim(closing.anchor, open);1517 self.scrim(anchor, open);
1485 let grows = motion == PopupMotion::Grow;1518 let grows = motion == PopupMotion::Grow;
1486 let motion = closing.anchor.motion(closing.rect, open, grows);1519 let motion = closing.placed.motion(open, grows);
1487 self.display.push(Display::Motion(Some(motion)));1520 self.display.push(Display::Motion(Some(motion)));
1488 self.display.extend(closing.display.iter().cloned());1521 self.display.extend(closing.display.iter().cloned());
1489 self.display.push(Display::Motion(None));1522 self.display.push(Display::Motion(None));
1490 if !self.painted.iter().any(|(shown, ..)| *shown == closing.id) {1523 if !self.painted.iter().any(|(shown, _)| shown.id == id) {
1491 self.closing.push(closing);1524 self.closing.push(closing);
1492 }1525 }
1493 }1526 }
...@@ -2050,6 +2083,7 @@ impl Built {...@@ -2050,6 +2083,7 @@ impl Built {
2050 offset: spec.offset,2083 offset: spec.offset,
2051 cursor: spec.cursor,2084 cursor: spec.cursor,
2052 anchor: spec.anchor,2085 anchor: spec.anchor,
2086 around: [0.0; 4],
2053 open: 1.0,2087 open: 1.0,
2054 motion: None,2088 motion: None,
2055 marks: Vec::new(),2089 marks: Vec::new(),
crates/ui/src/list.rs+1-1
...@@ -96,7 +96,7 @@ pub fn list<R: Rows>(...@@ -96,7 +96,7 @@ pub fn list<R: Rows>(
96 }96 }
97 low97 low
98 };98 };
99 let rect = ui.rect(id);99 let rect = ui.laid_out(id);
100 let view = f64::from(match spec.size[1].size {100 let view = f64::from(match spec.size[1].size {
101 Size::Pixels(pixels) => pixels,101 Size::Pixels(pixels) => pixels,
102 _ => rect.map_or(0.0, |rect| rect[3] - rect[1]),102 _ => rect.map_or(0.0, |rect| rect[3] - rect[1]),
crates/ui/src/popup.rs+26-51
...@@ -117,7 +117,7 @@ pub fn menu(...@@ -117,7 +117,7 @@ pub fn menu(
117 .map_or(0.0, |badge| crate::badge_width(ui, badge) + ICON_GAP)117 .map_or(0.0, |badge| crate::badge_width(ui, badge) + ICON_GAP)
118 })118 })
119 .collect();119 .collect();
120 let window = ui.rect(Id::ROOT).map_or(0.0, |window| window[3]);120 let window = ui.size()[1];
121 let field = if filter.is_some() {121 let field = if filter.is_some() {
122 ROW + style.pad122 ROW + style.pad
123 } else {123 } else {
...@@ -154,11 +154,6 @@ pub fn menu(...@@ -154,11 +154,6 @@ pub fn menu(
154 0.0154 0.0
155 }155 }
156 + 2.0 * (style.pad + style.row_pad);156 + 2.0 * (style.pad + style.row_pad);
157 let least = match anchor {
158 Anchor::Below(rect) => rect[2] - rect[0],
159 Anchor::Over(rect) => rect[2] - rect[0] + 2.0 * style.pad,
160 _ => 0.0,
161 };
162 choose(157 choose(
163 ui,158 ui,
164 id,159 id,
...@@ -166,7 +161,7 @@ pub fn menu(...@@ -166,7 +161,7 @@ pub fn menu(
166 anchor,161 anchor,
167 &[("", items)],162 &[("", items)],
168 filter,163 filter,
169 width.max(least).max(NARROWEST),164 width.max(NARROWEST),
170 style.row,165 style.row,
171 )166 )
172 .map(|(_, index)| index)167 .map(|(_, index)| index)
...@@ -194,7 +189,7 @@ pub fn submenus(ui: &mut Ui, id: Id, items: &[Item], submenu: impl Fn(usize) ->...@@ -194,7 +189,7 @@ pub fn submenus(ui: &mut Ui, id: Id, items: &[Item], submenu: impl Fn(usize) ->
194 highlight.is_some_and(opens) && !navigation(ui, &[id], &[NamedKey::ArrowRight]).is_empty();189 highlight.is_some_and(opens) && !navigation(ui, &[id], &[NamedKey::ArrowRight]).is_empty();
195 let under = highlight.filter(|key| {190 let under = highlight.filter(|key| {
196 ui.pointer191 ui.pointer
197 .zip(ui.rect(rows.child(*key)))192 .zip(ui.laid_out(rows.child(*key)))
198 .is_some_and(|(point, rect)| crate::contains(rect, point))193 .is_some_and(|(point, rect)| crate::contains(rect, point))
199 });194 });
200 let now = ui.now;195 let now = ui.now;
...@@ -225,23 +220,23 @@ pub fn submenus(ui: &mut Ui, id: Id, items: &[Item], submenu: impl Fn(usize) ->...@@ -225,23 +220,23 @@ pub fn submenus(ui: &mut Ui, id: Id, items: &[Item], submenu: impl Fn(usize) ->
225}220}
226221
227/// Shows `title`, with the `keys` that run it and a `description` under it, in a tooltip222/// Shows `title`, with the `keys` that run it and a `description` under it, in a tooltip
228/// below the box built last while the pointer rests on it or on a box inside it: after a223/// below the box built last while the pointer rests on it or on a box inside it:
229/// delay, or at once while another has just shown. A press or the wheel hides it until the224/// after a delay, or at once while another has just shown. A press or the wheel hides it
230/// pointer leaves. It names the box to assistive technology, or where the box has no role,225/// until the pointer leaves. It names the box to assistive technology, or where the box has
231/// the unnamed controls inside it.226/// no role, the unnamed controls inside it.
232pub fn tooltip(ui: &mut Ui, title: &str, keys: &str, description: Option<&str>) {227pub fn tooltip(ui: &mut Ui, title: &str, keys: &str, description: Option<&str>) {
233 tooltip_below(ui, None, title, keys, description);228 tooltip_below(ui, None, title, keys, description);
234}229}
235230
236/// A `tooltip` below `part`, a window rectangle within the box built last, as a custom231/// A `tooltip` below `part`, a box built within the box built last, as a custom box names
237/// box names what it draws.232/// part of what it draws.
238pub fn tooltip_over(ui: &mut Ui, part: [f32; 4], title: &str, description: Option<&str>) {233pub fn tooltip_over(ui: &mut Ui, part: Id, title: &str, description: Option<&str>) {
239 tooltip_below(ui, Some(part), title, "", description);234 tooltip_below(ui, Some(part), title, "", description);
240}235}
241236
242fn tooltip_below(237fn tooltip_below(
243 ui: &mut Ui,238 ui: &mut Ui,
244 part: Option<[f32; 4]>,239 part: Option<Id>,
245 title: &str,240 title: &str,
246 keys: &str,241 keys: &str,
247 description: Option<&str>,242 description: Option<&str>,
...@@ -312,7 +307,6 @@ fn tooltip_below(...@@ -312,7 +307,6 @@ fn tooltip_below(
312 } else {307 } else {
313 format!("{title} ({keys})")308 format!("{title} ({keys})")
314 };309 };
315 let [left, top, right, bottom] = part.or(ui.rect(id)).unwrap_or_default();
316 let theme = &ui.theme;310 let theme = &ui.theme;
317 let spec = Spec {311 let spec = Spec {
318 axis: Axis::Y,312 axis: Axis::Y,
...@@ -322,7 +316,7 @@ fn tooltip_below(...@@ -322,7 +316,7 @@ fn tooltip_below(
322 radius: 4.0,316 radius: 4.0,
323 pad: [8.0, 5.0],317 pad: [8.0, 5.0],
324 gap: 3.0,318 gap: 3.0,
325 anchor: Some(Anchor::Below([left, top, right, bottom + PAD])),319 anchor: Some(Anchor::Below(part.unwrap_or(id))),
326 ..Spec::default()320 ..Spec::default()
327 };321 };
328 ui.open_as(id.child("tooltip"), spec);322 ui.open_as(id.child("tooltip"), spec);
...@@ -379,8 +373,7 @@ pub fn palette(ui: &mut Ui, id: Id, modes: &[(&str, &[Item])], placeholder: &str...@@ -379,8 +373,7 @@ pub fn palette(ui: &mut Ui, id: Id, modes: &[(&str, &[Item])], placeholder: &str
379 (ui.signals.get(&rows.child(*key))).is_some_and(|signal| signal.context.is_some())373 (ui.signals.get(&rows.child(*key))).is_some_and(|signal| signal.context.is_some())
380 });374 });
381 let chord = chord(ui, id.child("filter"), "k");375 let chord = chord(ui, id.child("filter"), "k");
382 let window = ui.rect(Id::ROOT).unwrap_or_default();376 let width = PALETTE.min(ui.size()[0] - 8.0 * PAD).max(NARROWEST);
383 let width = PALETTE.min(window[2] - 8.0 * PAD).max(NARROWEST);
384 if let Some((mode, index)) = choose(377 if let Some((mode, index)) = choose(
385 ui,378 ui,
386 id,379 id,
...@@ -461,9 +454,9 @@ fn choose(...@@ -461,9 +454,9 @@ fn choose(
461 let theme = ui.theme.clone();454 let theme = ui.theme.clone();
462 let style = theme.menu();455 let style = theme.menu();
463 // Over a combo box, the field takes the box's place at once, widening with the popup.456 // Over a combo box, the field takes the box's place at once, widening with the popup.
464 let (flags, height) = match anchor {457 let flags = match anchor {
465 Anchor::Over(rect) => (Flags::STILL, rect[3] - rect[1]),458 Anchor::Over(_) => Flags::STILL,
466 _ => (Flags::default(), ROW),459 _ => Flags::default(),
467 };460 };
468 surface(ui, id, role, anchor, width);461 surface(ui, id, role, anchor, width);
469 if let Some(placeholder) = filter {462 if let Some(placeholder) = filter {
...@@ -475,7 +468,7 @@ fn choose(...@@ -475,7 +468,7 @@ fn choose(
475 placeholder,468 placeholder,
476 Spec {469 Spec {
477 flags,470 flags,
478 size: [fill(), px(height)],471 size: [fill(), px(ROW)],
479 fill: Some(theme.base),472 fill: Some(theme.base),
480 border: Some(theme.accent),473 border: Some(theme.accent),
481 radius: 4.0,474 radius: 4.0,
...@@ -512,7 +505,7 @@ fn choose(...@@ -512,7 +505,7 @@ fn choose(
512 };505 };
513 first.map(|row| matches.key(row))506 first.map(|row| matches.key(row))
514 });507 });
515 let window = ui.rect(Id::ROOT).map_or(0.0, |window| window[3]);508 let window = ui.size()[1];
516 let field = if filter.is_some() {509 let field = if filter.is_some() {
517 ROW + style.pad510 ROW + style.pad
518 } else {511 } else {
...@@ -1208,7 +1201,7 @@ pub fn color_picker(...@@ -1208,7 +1201,7 @@ pub fn color_picker(
1208 let mut picked = state(ui, id).picked.unwrap_or_else(|| to_hsl(initial));1201 let mut picked = state(ui, id).picked.unwrap_or_else(|| to_hsl(initial));
1209 let pointer = ui.pointer();1202 let pointer = ui.pointer();
1210 let along = |ui: &Ui, part: Id| {1203 let along = |ui: &Ui, part: Id| {
1211 let [left, top, right, bottom] = ui.rect(part)?;1204 let [left, top, right, bottom] = ui.laid_out(part)?;
1212 let [x, y] = pointer?;1205 let [x, y] = pointer?;
1213 Some([1206 Some([
1214 ((x - left) / (right - left)).clamp(0.0, 1.0),1207 ((x - left) / (right - left)).clamp(0.0, 1.0),
...@@ -1412,33 +1405,15 @@ fn from_hsl([hue, saturation, lightness]: [f32; 3]) -> [u8; 3] {...@@ -1412,33 +1405,15 @@ fn from_hsl([hue, saturation, lightness]: [f32; 3]) -> [u8; 3] {
1412 [red, green, blue].map(|channel| ((channel + base) * 255.0).round().clamp(0.0, 255.0) as u8)1405 [red, green, blue].map(|channel| ((channel + base) * 255.0).round().clamp(0.0, 255.0) as u8)
1413}1406}
14141407
1415/// Opens popup `id`'s panel, a `role` `width` wide beside `anchor`; the caller closes it.1408/// Opens popup `id`'s panel, a `role` `width` wide beside `anchor`, or as a submenu beside
1409/// its row; the caller closes it.
1416fn surface(ui: &mut Ui, id: Id, role: Role, anchor: Anchor, width: f32) {1410fn surface(ui: &mut Ui, id: Id, role: Role, anchor: Anchor, width: f32) {
1417 let style = ui.theme.menu();1411 let style = ui.theme.menu();
1418 let pad = style.pad;
1419 // Level with its row, past the menu's edges.
1420 let beside = ui1412 let beside = ui
1421 .popups1413 .popups
1422 .iter()1414 .iter()
1423 .position(|popup| popup.id == id)1415 .find(|popup| popup.id == id)
1424 .and_then(|at| {1416 .and_then(|popup| popup.beside);
1425 let [_, top, _, bottom] = ui.rect(ui.popups[at].beside?)?;
1426 let [left, _, right, _] = ui.rect(ui.popups[at.checked_sub(1)?].id)?;
1427 Some([left, top, right, bottom])
1428 });
1429 let anchor = match beside.map_or(anchor, Anchor::Right) {
1430 Anchor::Below([left, top, right, bottom]) => {
1431 Anchor::Below([left, top - PAD, right, bottom + PAD])
1432 }
1433 // Level with the row it opens from, or with its contents over the box.
1434 Anchor::Right([left, top, right, bottom]) => {
1435 Anchor::Right([left, top - pad, right, bottom + pad])
1436 }
1437 Anchor::Over([left, top, right, bottom]) => {
1438 Anchor::Over([left - pad, top - pad, right + pad, bottom + pad])
1439 }
1440 point => point,
1441 };
1442 let spec = Spec {1417 let spec = Spec {
1443 axis: Axis::Y,1418 axis: Axis::Y,
1444 size: [px(width), children()],1419 size: [px(width), children()],
...@@ -1446,9 +1421,9 @@ fn surface(ui: &mut Ui, id: Id, role: Role, anchor: Anchor, width: f32) {...@@ -1446,9 +1421,9 @@ fn surface(ui: &mut Ui, id: Id, role: Role, anchor: Anchor, width: f32) {
1446 border: Some(style.border),1421 border: Some(style.border),
1447 shadow: Some(ui.theme.shadow),1422 shadow: Some(ui.theme.shadow),
1448 radius: style.radius,1423 radius: style.radius,
1449 pad: [pad; 2],1424 pad: [style.pad; 2],
1450 gap: pad,1425 gap: style.pad,
1451 anchor: Some(anchor),1426 anchor: Some(beside.map_or(anchor, Anchor::Right)),
1452 role: Some(role),1427 role: Some(role),
1453 ..Spec::default()1428 ..Spec::default()
1454 };1429 };
crates/ui/src/shell.rs+25-10
...@@ -54,8 +54,9 @@ const EDGE: f32 = 20.0;...@@ -54,8 +54,9 @@ const EDGE: f32 = 20.0;
54/// The row is as wide as the tabs, and gives up all of that where its own row lacks room.54/// The row is as wide as the tabs, and gives up all of that where its own row lacks room.
55/// Then the tabs scroll sideways under the wheel, either way it turns, fading out at the55/// Then the tabs scroll sideways under the wheel, either way it turns, fading out at the
56/// ends they are cut at into the row's `fill`, and the open tab scrolls into view as it56/// ends they are cut at into the row's `fill`, and the open tab scrolls into view as it
57/// opens. A tab given `true` after its colour shows the unread dot in its leading pad.57/// opens. A tab given `true` after its colour shows the unread dot in its leading pad. The tab
58#[allow(clippy::too_many_arguments)]58/// `renaming` holds what its builder builds inside it, given the tab's height.
59#[allow(clippy::too_many_arguments, clippy::type_complexity)]
59pub fn section_tabs(60pub fn section_tabs(
60 ui: &mut Ui,61 ui: &mut Ui,
61 row: Id,62 row: Id,
...@@ -63,6 +64,7 @@ pub fn section_tabs(...@@ -63,6 +64,7 @@ pub fn section_tabs(
63 active: usize,64 active: usize,
64 lit: Option<usize>,65 lit: Option<usize>,
65 dragged: Option<Dragged>,66 dragged: Option<Dragged>,
67 mut renaming: Option<(usize, &mut dyn FnMut(&mut Ui, f32))>,
66 section: &Section,68 section: &Section,
67 height: f32,69 height: f32,
68 fill: [f32; 4],70 fill: [f32; 4],
...@@ -83,7 +85,7 @@ pub fn section_tabs(...@@ -83,7 +85,7 @@ pub fn section_tabs(
83 .collect();85 .collect();
84 // The last tab's slant and shadow reach past its box.86 // The last tab's slant and shadow reach past its box.
85 let wide = left + lean(height) + crate::SHADOW[0];87 let wide = left + lean(height) + crate::SHADOW[0];
86 let view = ui.rect(row).map_or(wide, |rect| rect[2] - rect[0]);88 let view = ui.laid_out(row).map_or(wide, |rect| rect[2] - rect[0]);
87 let most = (wide - view).max(0.0);89 let most = (wide - view).max(0.0);
88 let [scroll, shown, shift] = [row.child("scroll"), row.child("shown"), row.child("offset")];90 let [scroll, shown, shift] = [row.child("scroll"), row.child("shown"), row.child("offset")];
89 let held_value = |ui: &Ui, id: Id| ui.states.get(&id).and_then(|state| state.tween);91 let held_value = |ui: &Ui, id: Id| ui.states.get(&id).and_then(|state| state.tween);
...@@ -190,7 +192,7 @@ pub fn section_tabs(...@@ -190,7 +192,7 @@ pub fn section_tabs(
190 let top = if index == active { section } else { &colors };192 let top = if index == active { section } else { &colors };
191 let fill = mix(colors.tab, top.frame[0], open);193 let fill = mix(colors.tab, top.frame[0], open);
192 let fade = |color: [f32; 4], alpha: f32| [color[0], color[1], color[2], color[3] * alpha];194 let fade = |color: [f32; 4], alpha: f32| [color[0], color[1], color[2], color[3] * alpha];
193 let signal = ui.leaf(195 let id = ui.open(
194 ("tab", index),196 ("tab", index),
195 Spec {197 Spec {
196 flags: Flags::CLICKABLE | Flags::FLOAT,198 flags: Flags::CLICKABLE | Flags::FLOAT,
...@@ -215,6 +217,11 @@ pub fn section_tabs(...@@ -215,6 +217,11 @@ pub fn section_tabs(
215 ..Spec::default()217 ..Spec::default()
216 },218 },
217 );219 );
220 if let Some((_, build)) = renaming.as_mut().filter(|(at, _)| *at == index) {
221 build(ui, tall);
222 }
223 ui.close();
224 let signal = ui.signal(id);
218 if unread {225 if unread {
219 let position = [226 let position = [
220 x - offset + (TAB_PAD - DOT) / 2.0,227 x - offset + (TAB_PAD - DOT) / 2.0,
...@@ -548,7 +555,7 @@ pub fn more_button(ui: &mut Ui, part: impl Hash, menu: Id, enabled: bool) -> Anc...@@ -548,7 +555,7 @@ pub fn more_button(ui: &mut Ui, part: impl Hash, menu: Id, enabled: bool) -> Anc
548 if enabled && ui.signal(id).pressed {555 if enabled && ui.signal(id).pressed {
549 ui.open_popup(menu);556 ui.open_popup(menu);
550 }557 }
551 Anchor::Below(ui.rect(id).unwrap_or_default())558 Anchor::Below(id)
552}559}
553560
554/// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle561/// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle
...@@ -593,15 +600,23 @@ pub fn menu_button(...@@ -593,15 +600,23 @@ pub fn menu_button(
593 },600 },
594 );601 );
595 }602 }
603 // The menu opens from the button shifted so its icons line up under the button's.
604 let style = ui.theme.menu();
605 let from = ui.id("menu");
606 ui.leaf(
607 "menu",
608 Spec {
609 flags: Flags::FLOAT,
610 size: [px(TOOL + ARROW), px(TOOL)],
611 position: [(TOOL - crate::ICON) / 2.0 - style.pad - style.row_pad, 0.0],
612 ..Spec::default()
613 },
614 );
596 ui.close();615 ui.close();
597 if ui.signal(id).pressed {616 if ui.signal(id).pressed {
598 ui.open_popup(menu);617 ui.open_popup(menu);
599 }618 }
600 let [left, top, right, bottom] = ui.rect(id).unwrap_or_default();619 Anchor::Below(from)
601 let menu = ui.theme.menu();
602 // The menu's icons line up under the button's.
603 let shift = menu.pad + menu.row_pad - (TOOL - crate::ICON) / 2.0;
604 Anchor::Below([left - shift, top, right - shift, bottom])
605}620}
606621
607/// A drop-down box `name`d, `width` wide, showing `text`, that opens popup `menu`; where622/// A drop-down box `name`d, `width` wide, showing `text`, that opens popup `menu`; where
crates/ui/src/tests.rs+191-143
...@@ -62,8 +62,8 @@ fn fill_yields_to_strict_siblings_along_the_flow() {...@@ -62,8 +62,8 @@ fn fill_yields_to_strict_siblings_along_the_flow() {
62 ui.close();62 ui.close();
63 ui.close();63 ui.close();
64 });64 });
65 assert_eq!(ui.rect(ids[0]), Some([0.0, 0.0, 280.0, 100.0]));65 assert_eq!(ui.laid_out(ids[0]), Some([0.0, 0.0, 280.0, 100.0]));
66 assert_eq!(ui.rect(ids[1]), Some([280.0, 0.0, 400.0, 100.0]));66 assert_eq!(ui.laid_out(ids[1]), Some([280.0, 0.0, 400.0, 100.0]));
67}67}
6868
69#[test]69#[test]
...@@ -93,7 +93,7 @@ fn padding_wider_than_its_box_leaves_loose_children_empty_not_negative() {...@@ -93,7 +93,7 @@ fn padding_wider_than_its_box_leaves_loose_children_empty_not_negative() {
93 ui.close();93 ui.close();
94 ui.close();94 ui.close();
95 });95 });
96 assert_eq!(ui.rect(child.unwrap()), Some([8.0, 8.0, 8.0, 8.0]));96 assert_eq!(ui.laid_out(child.unwrap()), Some([8.0, 8.0, 8.0, 8.0]));
97}97}
9898
99#[test]99#[test]
...@@ -128,8 +128,8 @@ fn children_sum_with_gaps_and_padding() {...@@ -128,8 +128,8 @@ fn children_sum_with_gaps_and_padding() {
128 ui.close();128 ui.close();
129 ui.close();129 ui.close();
130 });130 });
131 assert_eq!(ui.rect(column.unwrap()), Some([0.0, 0.0, 58.0, 44.0]));131 assert_eq!(ui.laid_out(column.unwrap()), Some([0.0, 0.0, 58.0, 44.0]));
132 assert_eq!(ui.rect(second.unwrap()), Some([4.0, 18.0, 34.0, 38.0]));132 assert_eq!(ui.laid_out(second.unwrap()), Some([4.0, 18.0, 34.0, 38.0]));
133}133}
134134
135/// A row `width` wide of a space, a box that yields all but a quarter of its 80 pixels, and135/// A row `width` wide of a space, a box that yields all but a quarter of its 80 pixels, and
...@@ -191,7 +191,7 @@ fn folding_row(ui: &mut Ui, width: f32, priorities: &[u32]) -> Vec<Option<[f32;...@@ -191,7 +191,7 @@ fn folding_row(ui: &mut Ui, width: f32, priorities: &[u32]) -> Vec<Option<[f32;
191 }191 }
192 ui.close();192 ui.close();
193 });193 });
194 ids.into_iter().map(|id| ui.rect(id)).collect()194 ids.into_iter().map(|id| ui.laid_out(id)).collect()
195}195}
196196
197#[test]197#[test]
...@@ -296,12 +296,12 @@ fn a_group_inside_a_box_sized_by_its_children_folds_with_the_row() {...@@ -296,12 +296,12 @@ fn a_group_inside_a_box_sized_by_its_children_folds_with_the_row() {
296 };296 };
297 let [outer, after] = ["outer", "later"].map(|part| Id::ROOT.child("row").child(part));297 let [outer, after] = ["outer", "later"].map(|part| Id::ROOT.child("row").child(part));
298 build(&mut ui, 180.0);298 build(&mut ui, 180.0);
299 assert_eq!(ui.rect(outer), Some([0.0, 0.0, 150.0, 20.0]));299 assert_eq!(ui.laid_out(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,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.301 // and the box around it narrows to 70.
302 build(&mut ui, 179.0);302 build(&mut ui, 179.0);
303 assert_eq!(ui.rect(outer), Some([0.0, 0.0, 70.0, 20.0]));303 assert_eq!(ui.laid_out(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]));304 assert_eq!(ui.laid_out(after), Some([70.0, 0.0, 100.0, 20.0]));
305 assert_eq!(ui.narrowest(Id::ROOT.child("row")), Some(100.0));305 assert_eq!(ui.narrowest(Id::ROOT.child("row")), Some(100.0));
306}306}
307307
...@@ -340,7 +340,7 @@ fn a_box_sized_by_its_children_yields_what_they_yield() {...@@ -340,7 +340,7 @@ fn a_box_sized_by_its_children_yields_what_they_yield() {
340 ui.close();340 ui.close();
341 ui.close();341 ui.close();
342 });342 });
343 assert_eq!(ui.rect(combo.unwrap()), Some([0.0, 0.0, 60.0, 20.0]));343 assert_eq!(ui.laid_out(combo.unwrap()), Some([0.0, 0.0, 60.0, 20.0]));
344}344}
345345
346#[test]346#[test]
...@@ -621,7 +621,7 @@ fn wheel_scrolls_within_the_content_and_clips_children() {...@@ -621,7 +621,7 @@ fn wheel_scrolls_within_the_content_and_clips_children() {
621 200.0,621 200.0,
622 "at once, as on the page"622 "at once, as on the page"
623 );623 );
624 assert_eq!(ui.rect(last.unwrap()), Some([0.0, 70.0, 100.0, 100.0]));624 assert_eq!(ui.laid_out(last.unwrap()), Some([0.0, 70.0, 100.0, 100.0]));
625 let layers = ui.layers();625 let layers = ui.layers();
626 assert!(matches!(626 assert!(matches!(
627 layers[..],627 layers[..],
...@@ -959,7 +959,7 @@ fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {...@@ -959,7 +959,7 @@ fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {
959 build(&mut ui, &mut offset);959 build(&mut ui, &mut offset);
960 build(&mut ui, &mut offset);960 build(&mut ui, &mut offset);
961 let thumb = Id::ROOT.child("view").child("bar");961 let thumb = Id::ROOT.child("view").child("bar");
962 let rect = ui.rect(thumb).unwrap();962 let rect = ui.laid_out(thumb).unwrap();
963 assert_eq!(963 assert_eq!(
964 rect,964 rect,
965 [190.0, 4.0, 196.0, 61.0],965 [190.0, 4.0, 196.0, 61.0],
...@@ -978,7 +978,7 @@ fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {...@@ -978,7 +978,7 @@ fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {
978 build(&mut ui, &mut offset);978 build(&mut ui, &mut offset);
979 assert_eq!(offset, 1000.0);979 assert_eq!(offset, 1000.0);
980 build(&mut ui, &mut offset);980 build(&mut ui, &mut offset);
981 assert_eq!(ui.rect(thumb).unwrap(), [190.0, 147.0, 196.0, 204.0]);981 assert_eq!(ui.laid_out(thumb).unwrap(), [190.0, 147.0, 196.0, 204.0]);
982 ui.event(Event::PointerMoved([193.0, -500.0]));982 ui.event(Event::PointerMoved([193.0, -500.0]));
983 build(&mut ui, &mut offset);983 build(&mut ui, &mut offset);
984 assert_eq!(offset, 0.0);984 assert_eq!(offset, 0.0);
...@@ -1010,7 +1010,7 @@ fn a_scrollbar_without_a_corner_ends_as_far_from_its_end_as_from_its_start() {...@@ -1010,7 +1010,7 @@ fn a_scrollbar_without_a_corner_ends_as_far_from_its_end_as_from_its_start() {
1010 ui.close();1010 ui.close();
1011 });1011 });
1012 }1012 }
1013 let rect = ui.rect(Id::ROOT.child("view").child("bar")).unwrap();1013 let rect = ui.laid_out(Id::ROOT.child("view").child("bar")).unwrap();
1014 assert_eq!(rect[3], 204.0);1014 assert_eq!(rect[3], 204.0);
1015}1015}
10161016
...@@ -1067,7 +1067,7 @@ fn system_scrollers_step_page_and_drag_by_the_parts_the_platform_paints() {...@@ -1067,7 +1067,7 @@ fn system_scrollers_step_page_and_drag_by_the_parts_the_platform_paints() {
1067 build(&mut ui, &mut offset);1067 build(&mut ui, &mut offset);
1068 let bar = Id::ROOT.child("view").child("bar");1068 let bar = Id::ROOT.child("view").child("bar");
1069 assert_eq!(1069 assert_eq!(
1070 ui.rect(bar).unwrap(),1070 ui.laid_out(bar).unwrap(),
1071 [185.0, 0.0, 200.0, 200.0],1071 [185.0, 0.0, 200.0, 200.0],
1072 "the corner stays clear"1072 "the corner stays clear"
1073 );1073 );
...@@ -1126,7 +1126,11 @@ fn menu_id() -> Id {...@@ -1126,7 +1126,11 @@ fn menu_id() -> Id {
11261126
1127/// Builds a frame with a button filling the window under the menu, and returns the1127/// Builds a frame with a button filling the window under the menu, and returns the
1128/// button's signal and the item chosen.1128/// button's signal and the item chosen.
1129fn menu_frame(ui: &mut Ui, anchor: Anchor, filter: Option<&str>) -> (Signal, Option<usize>) {1129fn menu_frame(
1130 ui: &mut Ui,
1131 anchor: impl FnOnce(&mut Ui) -> Anchor,
1132 filter: Option<&str>,
1133) -> (Signal, Option<usize>) {
1130 let mut result = (Signal::default(), None);1134 let mut result = (Signal::default(), None);
1131 frame(ui, |ui| {1135 frame(ui, |ui| {
1132 let under = ui.leaf(1136 let under = ui.leaf(
...@@ -1137,28 +1141,49 @@ fn menu_frame(ui: &mut Ui, anchor: Anchor, filter: Option<&str>) -> (Signal, Opt...@@ -1137,28 +1141,49 @@ fn menu_frame(ui: &mut Ui, anchor: Anchor, filter: Option<&str>) -> (Signal, Opt
1137 ..Spec::default()1141 ..Spec::default()
1138 },1142 },
1139 );1143 );
1144 let anchor = anchor(ui);
1140 let chosen = popup::menu(ui, menu_id(), anchor, &items(), filter);1145 let chosen = popup::menu(ui, menu_id(), anchor, &items(), filter);
1141 result = (under, chosen);1146 result = (under, chosen);
1142 });1147 });
1143 result1148 result
1144}1149}
11451150
1146const BELOW: Anchor = Anchor::Below([20.0, 20.0, 100.0, 40.0]);1151/// A box floating at `rect` in the window, built at its root, for a popup to open beside.
1152fn spot(ui: &mut Ui, rect: [f32; 4]) -> Id {
1153 let [left, top, right, bottom] = rect;
1154 let id = Id::ROOT.child(("spot", rect.map(f32::to_bits)));
1155 ui.open_as(
1156 id,
1157 Spec {
1158 flags: Flags::FLOAT,
1159 size: [px(right - left), px(bottom - top)],
1160 position: [left, top],
1161 ..Spec::default()
1162 },
1163 );
1164 ui.close();
1165 id
1166}
11471167
1148/// A window with the menu open and faded in below `BELOW`.1168/// Below a box at (20, 20), 80 by 20.
1169fn below(ui: &mut Ui) -> Anchor {
1170 Anchor::Below(spot(ui, [20.0, 20.0, 100.0, 40.0]))
1171}
1172
1173/// A window with the menu open and faded in `below`.
1149fn open_menu(filter: Option<&str>) -> Ui {1174fn open_menu(filter: Option<&str>) -> Ui {
1150 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);1175 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1151 menu_frame(&mut ui, BELOW, filter);1176 menu_frame(&mut ui, below, filter);
1152 ui.open_popup(menu_id());1177 ui.open_popup(menu_id());
1153 for _ in 0..40 {1178 for _ in 0..40 {
1154 menu_frame(&mut ui, BELOW, filter);1179 menu_frame(&mut ui, below, filter);
1155 }1180 }
1156 ui1181 ui
1157}1182}
11581183
1159/// The centre of the menu's row for `item`, from the latest layout.1184/// The centre of the menu's row for `item`, from the latest layout.
1160fn row(ui: &Ui, item: usize) -> [f32; 2] {1185fn row(ui: &Ui, item: usize) -> [f32; 2] {
1161 let rect = ui.rect(menu_id().child("rows").child(item)).unwrap();1186 let rect = ui.laid_out(menu_id().child("rows").child(item)).unwrap();
1162 [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0]1187 [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0]
1163}1188}
11641189
...@@ -1189,7 +1214,7 @@ fn tip_frame(ui: &mut Ui) -> [Option<[f32; 4]>; 2] {...@@ -1189,7 +1214,7 @@ fn tip_frame(ui: &mut Ui) -> [Option<[f32; 4]>; 2] {
1189 tips[index] = id.child("tooltip");1214 tips[index] = id.child("tooltip");
1190 }1215 }
1191 });1216 });
1192 tips.map(|tip| ui.rect(tip))1217 tips.map(|tip| ui.laid_out(tip))
1193}1218}
11941219
1195/// Frames the pointer rests through for `millis`, returning the tooltips as they end.1220/// Frames the pointer rests through for `millis`, returning the tooltips as they end.
...@@ -1241,10 +1266,18 @@ fn tooltips_wait_then_switch_at_once_and_hide_on_press_until_left() {...@@ -1241,10 +1266,18 @@ fn tooltips_wait_then_switch_at_once_and_hide_on_press_until_left() {
1241 assert!(rest(&mut ui, 500)[0].is_some());1266 assert!(rest(&mut ui, 500)[0].is_some());
1242}1267}
12431268
1269#[test]
1270fn a_popup_opens_beside_a_box_first_laid_out_in_the_same_frame() {
1271 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1272 ui.open_popup(menu_id());
1273 menu_frame(&mut ui, below, None);
1274 assert_eq!(ui.laid_out(menu_id()).unwrap()[..2], [20.0, 44.0]);
1275}
1276
1244#[test]1277#[test]
1245fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() {1278fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() {
1246 let mut ui = open_menu(None);1279 let mut ui = open_menu(None);
1247 let rect = ui.rect(menu_id()).unwrap();1280 let rect = ui.laid_out(menu_id()).unwrap();
1248 assert_eq!(1281 assert_eq!(
1249 [rect[0], rect[1]],1282 [rect[0], rect[1]],
1250 [20.0, 44.0],1283 [20.0, 44.0],
...@@ -1252,17 +1285,19 @@ fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() {...@@ -1252,17 +1285,19 @@ fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() {
1252 );1285 );
1253 assert_eq!(rect[2] - rect[0], 140.0, "no narrower than a menu");1286 assert_eq!(rect[2] - rect[0], 140.0, "no narrower than a menu");
1254 let height = rect[3] - rect[1];1287 let height = rect[3] - rect[1];
1255 menu_frame(&mut ui, Anchor::Below([300.0, 260.0, 380.0, 280.0]), None);1288 let corner = |ui: &mut Ui| Anchor::Below(spot(ui, [300.0, 260.0, 380.0, 280.0]));
1256 menu_frame(&mut ui, Anchor::Below([300.0, 260.0, 380.0, 280.0]), None);1289 menu_frame(&mut ui, corner, None);
1290 menu_frame(&mut ui, corner, None);
1257 assert_eq!(1291 assert_eq!(
1258 ui.rect(menu_id()),1292 ui.laid_out(menu_id()),
1259 Some([240.0, 256.0 - height, 380.0, 256.0]),1293 Some([240.0, 256.0 - height, 380.0, 256.0]),
1260 "above, level with the anchor's far edge"1294 "above, level with the anchor's far edge"
1261 );1295 );
1262 menu_frame(&mut ui, Anchor::Right([350.0, 100.0, 390.0, 120.0]), None);1296 let edge = |ui: &mut Ui| Anchor::Right(spot(ui, [350.0, 100.0, 390.0, 120.0]));
1263 menu_frame(&mut ui, Anchor::Right([350.0, 100.0, 390.0, 120.0]), None);1297 menu_frame(&mut ui, edge, None);
1298 menu_frame(&mut ui, edge, None);
1264 assert_eq!(1299 assert_eq!(
1265 ui.rect(menu_id()),1300 ui.laid_out(menu_id()),
1266 Some([210.0, 96.0, 350.0, 96.0 + height]),1301 Some([210.0, 96.0, 350.0, 96.0 + height]),
1267 "a submenu that cannot fit to the right opens to the left, its first row level"1302 "a submenu that cannot fit to the right opens to the left, its first row level"
1268 );1303 );
...@@ -1280,7 +1315,8 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {...@@ -1280,7 +1315,8 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
1280 let narrow = [100.0, 300.0];1315 let narrow = [100.0, 300.0];
1281 let menu = |ui: &mut Ui| {1316 let menu = |ui: &mut Ui| {
1282 sized_frame(ui, narrow, |ui| {1317 sized_frame(ui, narrow, |ui| {
1283 popup::menu(ui, menu_id(), BELOW, &items(), None);1318 let anchor = below(ui);
1319 popup::menu(ui, menu_id(), anchor, &items(), None);
1284 });1320 });
1285 };1321 };
1286 menu(&mut ui);1322 menu(&mut ui);
...@@ -1288,14 +1324,14 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {...@@ -1288,14 +1324,14 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
1288 for _ in 0..40 {1324 for _ in 0..40 {
1289 menu(&mut ui);1325 menu(&mut ui);
1290 }1326 }
1291 let rect = ui.rect(menu_id()).unwrap();1327 let rect = ui.laid_out(menu_id()).unwrap();
1292 assert!(inside(rect, narrow), "{rect:?} leaves the window's margin");1328 assert!(inside(rect, narrow), "{rect:?} leaves the window's margin");
1293 assert_eq!(1329 assert_eq!(
1294 rect[2] - rect[0],1330 rect[2] - rect[0],
1295 100.0 - 2.0 * POPUP_MARGIN,1331 100.0 - 2.0 * POPUP_MARGIN,
1296 "as wide as fits"1332 "as wide as fits"
1297 );1333 );
1298 let row = ui.rect(menu_id().child("rows").child(0_u64)).unwrap();1334 let row = ui.laid_out(menu_id().child("rows").child(0_u64)).unwrap();
1299 assert!(row[2] <= rect[2], "its rows narrow with it");1335 assert!(row[2] <= rect[2], "its rows narrow with it");
13001336
1301 // A dialog taller than a short window scrolls what it holds.1337 // A dialog taller than a short window scrolls what it holds.
...@@ -1333,15 +1369,15 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {...@@ -1333,15 +1369,15 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
1333 for _ in 0..40 {1369 for _ in 0..40 {
1334 build(&mut ui);1370 build(&mut ui);
1335 }1371 }
1336 let rect = ui.rect(dialog).unwrap();1372 let rect = ui.laid_out(dialog).unwrap();
1337 assert!(inside(rect, short), "{rect:?} leaves the window's margin");1373 assert!(inside(rect, short), "{rect:?} leaves the window's margin");
1338 let first = ui.rect(dialog.child(0)).unwrap();1374 let first = ui.laid_out(dialog.child(0)).unwrap();
1339 ui.event(Event::PointerMoved([200.0, 60.0]));1375 ui.event(Event::PointerMoved([200.0, 60.0]));
1340 ui.event(Event::Wheel([0.0, -100.0]));1376 ui.event(Event::Wheel([0.0, -100.0]));
1341 for _ in 0..40 {1377 for _ in 0..40 {
1342 build(&mut ui);1378 build(&mut ui);
1343 }1379 }
1344 let scrolled = ui.rect(dialog.child(0)).unwrap();1380 let scrolled = ui.laid_out(dialog.child(0)).unwrap();
1345 assert!(scrolled[1] < first[1], "the wheel scrolls it");1381 assert!(scrolled[1] < first[1], "the wheel scrolls it");
1346 for layer in ui.layers() {1382 for layer in ui.layers() {
1347 let Layer::Primitives(layer) = layer else {1383 let Layer::Primitives(layer) = layer else {
...@@ -1361,32 +1397,32 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {...@@ -1361,32 +1397,32 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
1361#[test]1397#[test]
1362fn popups_show_at_once_by_their_anchor() {1398fn popups_show_at_once_by_their_anchor() {
1363 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);1399 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1364 menu_frame(&mut ui, BELOW, None);1400 menu_frame(&mut ui, below, None);
1365 ui.open_popup(menu_id());1401 ui.open_popup(menu_id());
1366 menu_frame(&mut ui, BELOW, None);1402 menu_frame(&mut ui, below, None);
1367 menu_frame(&mut ui, BELOW, None);1403 menu_frame(&mut ui, below, None);
1368 assert_eq!(1404 assert_eq!(
1369 ui.rect(menu_id()).unwrap()[1],1405 ui.laid_out(menu_id()).unwrap()[1],
1370 44.0,1406 44.0,
1371 "right under the anchor"1407 "right under the anchor"
1372 );1408 );
1373 ui.close_popup(menu_id());1409 ui.close_popup(menu_id());
1374 menu_frame(&mut ui, Anchor::Point([50.0, 50.0]), None);1410 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1375 ui.open_popup(menu_id());1411 ui.open_popup(menu_id());
1376 menu_frame(&mut ui, Anchor::Point([50.0, 50.0]), None);1412 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1377 menu_frame(&mut ui, Anchor::Point([50.0, 50.0]), None);1413 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1378 assert_eq!(ui.rect(menu_id()).unwrap()[..2], [50.0, 50.0]);1414 assert_eq!(ui.laid_out(menu_id()).unwrap()[..2], [50.0, 50.0]);
1379}1415}
13801416
1381#[test]1417#[test]
1382fn a_press_outside_dismisses_without_reaching_what_is_beneath() {1418fn a_press_outside_dismisses_without_reaching_what_is_beneath() {
1383 let mut ui = open_menu(None);1419 let mut ui = open_menu(None);
1384 click(&mut ui, [300.0, 250.0]);1420 click(&mut ui, [300.0, 250.0]);
1385 let (under, chosen) = menu_frame(&mut ui, BELOW, None);1421 let (under, chosen) = menu_frame(&mut ui, below, None);
1386 assert!(!ui.popup_open(menu_id()) && chosen.is_none());1422 assert!(!ui.popup_open(menu_id()) && chosen.is_none());
1387 assert!(!under.pressed && !under.clicked);1423 assert!(!under.pressed && !under.clicked);
1388 click(&mut ui, [300.0, 250.0]);1424 click(&mut ui, [300.0, 250.0]);
1389 let (under, _) = menu_frame(&mut ui, BELOW, None);1425 let (under, _) = menu_frame(&mut ui, below, None);
1390 assert!(under.clicked, "once closed, beneath takes presses again");1426 assert!(under.clicked, "once closed, beneath takes presses again");
1391}1427}
13921428
...@@ -1394,13 +1430,13 @@ fn a_press_outside_dismisses_without_reaching_what_is_beneath() {...@@ -1394,13 +1430,13 @@ fn a_press_outside_dismisses_without_reaching_what_is_beneath() {
1394fn escape_dismisses_and_returns_the_focus() {1430fn escape_dismisses_and_returns_the_focus() {
1395 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);1431 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1396 let page = Id::ROOT.child("under");1432 let page = Id::ROOT.child("under");
1397 menu_frame(&mut ui, BELOW, None);1433 menu_frame(&mut ui, below, None);
1398 ui.set_focus(Some(page));1434 ui.set_focus(Some(page));
1399 ui.open_popup(menu_id());1435 ui.open_popup(menu_id());
1400 menu_frame(&mut ui, BELOW, None);1436 menu_frame(&mut ui, below, None);
1401 assert_eq!(ui.focused(), Some(menu_id()));1437 assert_eq!(ui.focused(), Some(menu_id()));
1402 ui.event(key(NamedKey::Escape));1438 ui.event(key(NamedKey::Escape));
1403 menu_frame(&mut ui, BELOW, None);1439 menu_frame(&mut ui, below, None);
1404 assert!(!ui.popup_open(menu_id()));1440 assert!(!ui.popup_open(menu_id()));
1405 assert_eq!(ui.focused(), Some(page));1441 assert_eq!(ui.focused(), Some(page));
1406}1442}
...@@ -1409,11 +1445,11 @@ fn escape_dismisses_and_returns_the_focus() {...@@ -1409,11 +1445,11 @@ fn escape_dismisses_and_returns_the_focus() {
1409fn input_over_a_popup_stays_with_it() {1445fn input_over_a_popup_stays_with_it() {
1410 let mut ui = open_menu(None);1446 let mut ui = open_menu(None);
1411 ui.event(Event::PointerMoved(row(&ui, 1)));1447 ui.event(Event::PointerMoved(row(&ui, 1)));
1412 let (under, _) = menu_frame(&mut ui, BELOW, None);1448 let (under, _) = menu_frame(&mut ui, below, None);
1413 assert!(!under.hovered);1449 assert!(!under.hovered);
1414 let point = row(&ui, 1);1450 let point = row(&ui, 1);
1415 click(&mut ui, point);1451 click(&mut ui, point);
1416 let (under, chosen) = menu_frame(&mut ui, BELOW, None);1452 let (under, chosen) = menu_frame(&mut ui, below, None);
1417 assert_eq!(chosen, Some(1));1453 assert_eq!(chosen, Some(1));
1418 assert!(!under.pressed && !under.clicked);1454 assert!(!under.pressed && !under.clicked);
1419 assert!(!ui.popup_open(menu_id()), "choosing closes it");1455 assert!(!ui.popup_open(menu_id()), "choosing closes it");
...@@ -1424,7 +1460,7 @@ fn disabled_items_ignore_the_pointer() {...@@ -1424,7 +1460,7 @@ fn disabled_items_ignore_the_pointer() {
1424 let mut ui = open_menu(None);1460 let mut ui = open_menu(None);
1425 let point = row(&ui, 2);1461 let point = row(&ui, 2);
1426 click(&mut ui, point);1462 click(&mut ui, point);
1427 let (_, chosen) = menu_frame(&mut ui, BELOW, None);1463 let (_, chosen) = menu_frame(&mut ui, below, None);
1428 assert_eq!(chosen, None);1464 assert_eq!(chosen, None);
1429 assert!(ui.popup_open(menu_id()));1465 assert!(ui.popup_open(menu_id()));
1430}1466}
...@@ -1444,21 +1480,21 @@ fn keys_move_the_highlight_past_disabled_items() {...@@ -1444,21 +1480,21 @@ fn keys_move_the_highlight_past_disabled_items() {
1444 for named in keys {1480 for named in keys {
1445 ui.event(key(named));1481 ui.event(key(named));
1446 }1482 }
1447 menu_frame(&mut ui, BELOW, None);1483 menu_frame(&mut ui, below, None);
1448 assert_eq!(ui.popups[0].highlight, Some(highlight));1484 assert_eq!(ui.popups[0].highlight, Some(highlight));
1449 }1485 }
1450 ui.event(key(NamedKey::Enter));1486 ui.event(key(NamedKey::Enter));
1451 assert_eq!(menu_frame(&mut ui, BELOW, None).1, Some(3));1487 assert_eq!(menu_frame(&mut ui, below, None).1, Some(3));
1452}1488}
14531489
1454#[test]1490#[test]
1455fn the_pointer_moves_the_highlight_only_when_it_moves() {1491fn the_pointer_moves_the_highlight_only_when_it_moves() {
1456 let mut ui = open_menu(None);1492 let mut ui = open_menu(None);
1457 ui.event(Event::PointerMoved(row(&ui, 1)));1493 ui.event(Event::PointerMoved(row(&ui, 1)));
1458 menu_frame(&mut ui, BELOW, None);1494 menu_frame(&mut ui, below, None);
1459 ui.event(key(NamedKey::ArrowDown));1495 ui.event(key(NamedKey::ArrowDown));
1460 menu_frame(&mut ui, BELOW, None);1496 menu_frame(&mut ui, below, None);
1461 menu_frame(&mut ui, BELOW, None);1497 menu_frame(&mut ui, below, None);
1462 assert_eq!(1498 assert_eq!(
1463 ui.popups[0].highlight,1499 ui.popups[0].highlight,
1464 Some(3),1500 Some(3),
...@@ -1483,7 +1519,8 @@ fn headings_are_never_chosen_and_hide_while_filtered() {...@@ -1483,7 +1519,8 @@ fn headings_are_never_chosen_and_hide_while_filtered() {
1483 let build = |ui: &mut Ui| {1519 let build = |ui: &mut Ui| {
1484 let mut chosen = None;1520 let mut chosen = None;
1485 frame(ui, |ui| {1521 frame(ui, |ui| {
1486 chosen = popup::menu(ui, menu_id(), BELOW, &items, Some("Filter"));1522 let anchor = below(ui);
1523 chosen = popup::menu(ui, menu_id(), anchor, &items, Some("Filter"));
1487 });1524 });
1488 chosen1525 chosen
1489 };1526 };
...@@ -1516,7 +1553,7 @@ fn typing_filters_to_the_best_matches_first() {...@@ -1516,7 +1553,7 @@ fn typing_filters_to_the_best_matches_first() {
1516 }1553 }
1517 let settle = |ui: &mut Ui| {1554 let settle = |ui: &mut Ui| {
1518 for _ in 0..30 {1555 for _ in 0..30 {
1519 menu_frame(ui, BELOW, Some("Filter"));1556 menu_frame(ui, below, Some("Filter"));
1520 }1557 }
1521 };1558 };
1522 settle(&mut ui);1559 settle(&mut ui);
...@@ -1524,7 +1561,7 @@ fn typing_filters_to_the_best_matches_first() {...@@ -1524,7 +1561,7 @@ fn typing_filters_to_the_best_matches_first() {
1524 let shown = |ui: &Ui| {1561 let shown = |ui: &Ui| {
1525 let mut shown: Vec<_> = (0..4_usize)1562 let mut shown: Vec<_> = (0..4_usize)
1526 .filter_map(|item| {1563 .filter_map(|item| {
1527 let rect = ui.rect(menu_id().child("rows").child(item))?;1564 let rect = ui.laid_out(menu_id().child("rows").child(item))?;
1528 Some((rect[1] as i32, item))1565 Some((rect[1] as i32, item))
1529 })1566 })
1530 .collect();1567 .collect();
...@@ -1541,20 +1578,20 @@ fn typing_filters_to_the_best_matches_first() {...@@ -1541,20 +1578,20 @@ fn typing_filters_to_the_best_matches_first() {
1541 assert_eq!(shown(&ui), [0, 1, 3], "equal matches keep their order");1578 assert_eq!(shown(&ui), [0, 1, 3], "equal matches keep their order");
1542 ui.event(key(NamedKey::ArrowDown));1579 ui.event(key(NamedKey::ArrowDown));
1543 ui.event(key(NamedKey::Enter));1580 ui.event(key(NamedKey::Enter));
1544 assert_eq!(menu_frame(&mut ui, BELOW, Some("Filter")).1, Some(1));1581 assert_eq!(menu_frame(&mut ui, below, Some("Filter")).1, Some(1));
1545}1582}
15461583
1547#[test]1584#[test]
1548fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {1585fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {
1549 let mut ui = open_menu(Some("Filter"));1586 let mut ui = open_menu(Some("Filter"));
1550 ui.event(typed("z"));1587 ui.event(typed("z"));
1551 menu_frame(&mut ui, BELOW, Some("Filter"));1588 menu_frame(&mut ui, below, Some("Filter"));
1552 ui.event(key(NamedKey::ArrowDown));1589 ui.event(key(NamedKey::ArrowDown));
1553 let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter"));1590 let (_, chosen) = menu_frame(&mut ui, below, Some("Filter"));
1554 assert_eq!(chosen, None);1591 assert_eq!(chosen, None);
1555 assert!(ui.popup_open(menu_id()));1592 assert!(ui.popup_open(menu_id()));
1556 ui.event(key(NamedKey::Enter));1593 ui.event(key(NamedKey::Enter));
1557 let (_, chosen) = menu_frame(&mut ui, BELOW, Some("Filter"));1594 let (_, chosen) = menu_frame(&mut ui, below, Some("Filter"));
1558 assert_eq!(chosen, None);1595 assert_eq!(chosen, None);
1559 assert!(!ui.popup_open(menu_id()));1596 assert!(!ui.popup_open(menu_id()));
1560}1597}
...@@ -1579,7 +1616,7 @@ fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {...@@ -1579,7 +1616,7 @@ fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {
1579 for _ in 0..40 {1616 for _ in 0..40 {
1580 build(&mut ui);1617 build(&mut ui);
1581 }1618 }
1582 let rect = ui.rect(palette).unwrap();1619 let rect = ui.laid_out(palette).unwrap();
1583 assert_eq!(rect[0] + rect[2], 400.0, "centred");1620 assert_eq!(rect[0] + rect[2], 400.0, "centred");
1584 assert_eq!(rect[1], 300.0 / 8.0, "an eighth down");1621 assert_eq!(rect[1], 300.0 / 8.0, "an eighth down");
1585 ui.event(typed("a"));1622 ui.event(typed("a"));
...@@ -1704,7 +1741,7 @@ fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {...@@ -1704,7 +1741,7 @@ fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {
1704 ui.popup_open(palette) && !ui.popup_open(actions),1741 ui.popup_open(palette) && !ui.popup_open(actions),
1705 "Escape goes back to the palette"1742 "Escape goes back to the palette"
1706 );1743 );
1707 let row = ui.rect(palette.child("rows").child(3_u64)).unwrap();1744 let row = ui.laid_out(palette.child("rows").child(3_u64)).unwrap();
1708 ui.event(Event::PointerMoved([row[0] + 4.0, row[1] + 4.0]));1745 ui.event(Event::PointerMoved([row[0] + 4.0, row[1] + 4.0]));
1709 ui.event(Event::Button {1746 ui.event(Event::Button {
1710 button: MouseButton::Right,1747 button: MouseButton::Right,
...@@ -1718,7 +1755,7 @@ fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {...@@ -1718,7 +1755,7 @@ fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {
1718 ui.event(typed("c"));1755 ui.event(typed("c"));
1719 build(&mut ui);1756 build(&mut ui);
1720 assert_eq!(1757 assert_eq!(
1721 ui.rect(palette.child("rows").child(0_u64)),1758 ui.laid_out(palette.child("rows").child(0_u64)),
1722 None,1759 None,
1723 "a repeated item shows only while unfiltered"1760 "a repeated item shows only while unfiltered"
1724 );1761 );
...@@ -1734,7 +1771,8 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {...@@ -1734,7 +1771,8 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {
1734 let build = |ui: &mut Ui| {1771 let build = |ui: &mut Ui| {
1735 let mut chosen = None;1772 let mut chosen = None;
1736 frame(ui, |ui| {1773 frame(ui, |ui| {
1737 chosen = popup::colors(ui, grid, BELOW, "Automatic", &swatches, 3);1774 let anchor = below(ui);
1775 chosen = popup::colors(ui, grid, anchor, "Automatic", &swatches, 3);
1738 });1776 });
1739 chosen1777 chosen
1740 };1778 };
...@@ -1755,7 +1793,7 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {...@@ -1755,7 +1793,7 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {
1755 for _ in 0..40 {1793 for _ in 0..40 {
1756 build(&mut ui);1794 build(&mut ui);
1757 }1795 }
1758 let rect = ui.rect(grid.child(("cell", 0_usize))).unwrap();1796 let rect = ui.laid_out(grid.child(("cell", 0_usize))).unwrap();
1759 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);1797 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
1760 assert_eq!(build(&mut ui), Some(None));1798 assert_eq!(build(&mut ui), Some(None));
1761}1799}
...@@ -1767,7 +1805,8 @@ fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {...@@ -1767,7 +1805,8 @@ fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {
1767 let build = |ui: &mut Ui| {1805 let build = |ui: &mut Ui| {
1768 let mut chosen = None;1806 let mut chosen = None;
1769 frame(ui, |ui| {1807 frame(ui, |ui| {
1770 chosen = popup::table_picker(ui, grid, BELOW, [4, 3]);1808 let anchor = below(ui);
1809 chosen = popup::table_picker(ui, grid, anchor, [4, 3]);
1771 });1810 });
1772 chosen1811 chosen
1773 };1812 };
...@@ -1789,7 +1828,7 @@ fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {...@@ -1789,7 +1828,7 @@ fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {
1789 for _ in 0..40 {1828 for _ in 0..40 {
1790 build(&mut ui);1829 build(&mut ui);
1791 }1830 }
1792 let rect = ui.rect(grid.child(("cell", 5_usize))).unwrap();1831 let rect = ui.laid_out(grid.child(("cell", 5_usize))).unwrap();
1793 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);1832 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
1794 assert_eq!(build(&mut ui), Some([2, 2]));1833 assert_eq!(build(&mut ui), Some([2, 2]));
1795}1834}
...@@ -1804,7 +1843,7 @@ fn popups_opened_within_a_popup_stay_above_it() {...@@ -1804,7 +1843,7 @@ fn popups_opened_within_a_popup_stay_above_it() {
1804 menu_id(),1843 menu_id(),
1805 Spec {1844 Spec {
1806 size: [px(100.0), px(100.0)],1845 size: [px(100.0), px(100.0)],
1807 anchor: Some(BELOW),1846 anchor: Some(Anchor::Point([20.0, 44.0])),
1808 ..Spec::default()1847 ..Spec::default()
1809 },1848 },
1810 );1849 );
...@@ -1812,13 +1851,8 @@ fn popups_opened_within_a_popup_stay_above_it() {...@@ -1812,13 +1851,8 @@ fn popups_opened_within_a_popup_stay_above_it() {
1812 ui.open_popup(submenu);1851 ui.open_popup(submenu);
1813 }1852 }
1814 ui.close();1853 ui.close();
1815 popup::menu(1854 let anchor = Anchor::Right(spot(ui, [100.0, 50.0, 120.0, 70.0]));
1816 ui,1855 popup::menu(ui, submenu, anchor, &items(), None);
1817 submenu,
1818 Anchor::Right([100.0, 50.0, 120.0, 70.0]),
1819 &items(),
1820 None,
1821 );
1822 })1856 })
1823 };1857 };
1824 build(&mut ui, true);1858 build(&mut ui, true);
...@@ -1837,11 +1871,12 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho...@@ -1837,11 +1871,12 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho
1837 let build = |ui: &mut Ui| {1871 let build = |ui: &mut Ui| {
1838 let mut chosen = None;1872 let mut chosen = None;
1839 frame(ui, |ui| {1873 frame(ui, |ui| {
1840 chosen = popup::menu(ui, menu_id(), BELOW, &entries, None);1874 let anchor = below(ui);
1875 chosen = popup::menu(ui, menu_id(), anchor, &entries, None);
1841 popup::submenus(ui, menu_id(), &entries, |item| {1876 popup::submenus(ui, menu_id(), &entries, |item| {
1842 (item == 1).then_some(submenu)1877 (item == 1).then_some(submenu)
1843 });1878 });
1844 popup::menu(ui, submenu, BELOW, &items(), None);1879 popup::menu(ui, submenu, anchor, &items(), None);
1845 });1880 });
1846 chosen1881 chosen
1847 };1882 };
...@@ -1860,7 +1895,7 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho...@@ -1860,7 +1895,7 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho
1860 build(&mut ui);1895 build(&mut ui);
1861 }1896 }
1862 assert!(ui.popup_open(submenu), "once it rests");1897 assert!(ui.popup_open(submenu), "once it rests");
1863 let [menu, beside] = [menu_id(), submenu].map(|id| ui.rect(id).unwrap());1898 let [menu, beside] = [menu_id(), submenu].map(|id| ui.laid_out(id).unwrap());
1864 assert!(1899 assert!(
1865 beside[0] >= menu[2] - 1.0,1900 beside[0] >= menu[2] - 1.0,
1866 "beside the menu, not below the anchor"1901 "beside the menu, not below the anchor"
...@@ -1878,7 +1913,7 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho...@@ -1878,7 +1913,7 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho
1878 assert_eq!(build(&mut ui), None, "a click opens it, not a choice");1913 assert_eq!(build(&mut ui), None, "a click opens it, not a choice");
1879 build(&mut ui);1914 build(&mut ui);
1880 assert!(ui.popup_open(menu_id()) && ui.popup_open(submenu));1915 assert!(ui.popup_open(menu_id()) && ui.popup_open(submenu));
1881 let first = ui.rect(submenu.child("rows").child(0_u64)).unwrap();1916 let first = ui.laid_out(submenu.child("rows").child(0_u64)).unwrap();
1882 click(&mut ui, [first[0] + 4.0, first[1] + 4.0]);1917 click(&mut ui, [first[0] + 4.0, first[1] + 4.0]);
1883 build(&mut ui);1918 build(&mut ui);
1884 assert!(1919 assert!(
...@@ -1967,7 +2002,7 @@ fn list_frame(...@@ -1967,7 +2002,7 @@ fn list_frame(
1967}2002}
19682003
1969fn row_top(ui: &Ui, key: u64) -> Option<f32> {2004fn row_top(ui: &Ui, key: u64) -> Option<f32> {
1970 ui.rect(list_id().child(key)).map(|rect| rect[1])2005 ui.laid_out(list_id().child(key)).map(|rect| rect[1])
1971}2006}
19722007
1973/// Frames until the list's animations end.2008/// Frames until the list's animations end.
...@@ -2071,7 +2106,8 @@ fn a_menu_runs_to_the_window_edge_before_it_scrolls() {...@@ -2071,7 +2106,8 @@ fn a_menu_runs_to_the_window_edge_before_it_scrolls() {
2071 .collect();2106 .collect();
2072 let build = |ui: &mut Ui, height: f32| {2107 let build = |ui: &mut Ui, height: f32| {
2073 sized_frame(ui, [400.0, height], |ui| {2108 sized_frame(ui, [400.0, height], |ui| {
2074 popup::menu(ui, menu_id(), BELOW, &items, None);2109 let anchor = below(ui);
2110 popup::menu(ui, menu_id(), anchor, &items, None);
2075 })2111 })
2076 };2112 };
2077 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);2113 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
...@@ -2080,9 +2116,9 @@ fn a_menu_runs_to_the_window_edge_before_it_scrolls() {...@@ -2080,9 +2116,9 @@ fn a_menu_runs_to_the_window_edge_before_it_scrolls() {
2080 for _ in 0..3 {2116 for _ in 0..3 {
2081 build(&mut ui, 800.0);2117 build(&mut ui, 800.0);
2082 }2118 }
2083 let bar = |ui: &Ui| ui.rect(menu_id().child("rows").child("bar"));2119 let bar = |ui: &Ui| ui.laid_out(menu_id().child("rows").child("bar"));
2084 assert!(bar(&ui).is_none(), "twenty rows fit a tall window whole");2120 assert!(bar(&ui).is_none(), "twenty rows fit a tall window whole");
2085 assert!(ui.rect(menu_id().child("rows").child(19u64)).is_some());2121 assert!(ui.laid_out(menu_id().child("rows").child(19u64)).is_some());
2086 for _ in 0..3 {2122 for _ in 0..3 {
2087 build(&mut ui, 300.0);2123 build(&mut ui, 300.0);
2088 }2124 }
...@@ -2101,7 +2137,8 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {...@@ -2101,7 +2137,8 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {
2101 .collect();2137 .collect();
2102 let build = |ui: &mut Ui| {2138 let build = |ui: &mut Ui| {
2103 frame(ui, |ui| {2139 frame(ui, |ui| {
2104 popup::menu(ui, menu_id(), BELOW, &items, None);2140 let anchor = below(ui);
2141 popup::menu(ui, menu_id(), anchor, &items, None);
2105 })2142 })
2106 };2143 };
2107 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);2144 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
...@@ -2109,9 +2146,9 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {...@@ -2109,9 +2146,9 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {
2109 ui.open_popup(menu_id());2146 ui.open_popup(menu_id());
2110 build(&mut ui);2147 build(&mut ui);
2111 build(&mut ui);2148 build(&mut ui);
2112 let thumb = ui.rect(menu_id().child("rows").child("bar")).unwrap();2149 let thumb = ui.laid_out(menu_id().child("rows").child("bar")).unwrap();
2113 let top = ui2150 let top = ui
2114 .rect(menu_id().child("rows").child(0u64))2151 .laid_out(menu_id().child("rows").child(0u64))
2115 .map(|rect| rect[1]);2152 .map(|rect| rect[1]);
2116 let at = Instant::now();2153 let at = Instant::now();
2117 let x = (thumb[0] + thumb[2]) / 2.0;2154 let x = (thumb[0] + thumb[2]) / 2.0;
...@@ -2123,7 +2160,7 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {...@@ -2123,7 +2160,7 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {
2123 build(&mut ui);2160 build(&mut ui);
2124 assert!(ui.popup_open(menu_id()));2161 assert!(ui.popup_open(menu_id()));
2125 assert_ne!(2162 assert_ne!(
2126 ui.rect(menu_id().child("rows").child(0u64))2163 ui.laid_out(menu_id().child("rows").child(0u64))
2127 .map(|rect| rect[1]),2164 .map(|rect| rect[1]),
2128 top2165 top
2129 );2166 );
...@@ -2141,7 +2178,8 @@ fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end(...@@ -2141,7 +2178,8 @@ fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end(
2141 .collect();2178 .collect();
2142 let build = |ui: &mut Ui| {2179 let build = |ui: &mut Ui| {
2143 frame(ui, |ui| {2180 frame(ui, |ui| {
2144 popup::menu(ui, menu_id(), BELOW, &items, None);2181 let anchor = below(ui);
2182 popup::menu(ui, menu_id(), anchor, &items, None);
2145 })2183 })
2146 };2184 };
2147 let rows = menu_id().child("rows");2185 let rows = menu_id().child("rows");
...@@ -2152,7 +2190,7 @@ fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end(...@@ -2152,7 +2190,7 @@ fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end(
2152 build(&mut ui);2190 build(&mut ui);
2153 build(&mut ui);2191 build(&mut ui);
2154 assert_eq!(fade(&ui), [0.0, 12.0], "cut only at the bottom");2192 assert_eq!(fade(&ui), [0.0, 12.0], "cut only at the bottom");
2155 let thumb = ui.rect(rows.child("bar")).unwrap();2193 let thumb = ui.laid_out(rows.child("bar")).unwrap();
2156 let x = (thumb[0] + thumb[2]) / 2.0;2194 let x = (thumb[0] + thumb[2]) / 2.0;
2157 ui.event(Event::PointerMoved([x, thumb[1] + 2.0]));2195 ui.event(Event::PointerMoved([x, thumb[1] + 2.0]));
2158 press(&mut ui, Instant::now(), true);2196 press(&mut ui, Instant::now(), true);
...@@ -2161,8 +2199,8 @@ fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end(...@@ -2161,8 +2199,8 @@ fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end(
2161 build(&mut ui);2199 build(&mut ui);
2162 build(&mut ui);2200 build(&mut ui);
2163 assert_eq!(fade(&ui), [12.0, 0.0], "cut only at the top");2201 assert_eq!(fade(&ui), [12.0, 0.0], "cut only at the top");
2164 let list = ui.rect(rows).unwrap();2202 let list = ui.laid_out(rows).unwrap();
2165 let thumb = ui.rect(rows.child("bar")).unwrap();2203 let thumb = ui.laid_out(rows.child("bar")).unwrap();
2166 assert_eq!(2204 assert_eq!(
2167 thumb[3],2205 thumb[3],
2168 list[3] - 4.0,2206 list[3] - 4.0,
...@@ -2187,7 +2225,8 @@ fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() {...@@ -2187,7 +2225,8 @@ fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() {
2187 .collect();2225 .collect();
2188 let build = |ui: &mut Ui| {2226 let build = |ui: &mut Ui| {
2189 frame(ui, |ui| {2227 frame(ui, |ui| {
2190 popup::menu(ui, menu_id(), BELOW, &items, None);2228 let anchor = below(ui);
2229 popup::menu(ui, menu_id(), anchor, &items, None);
2191 })2230 })
2192 };2231 };
2193 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);2232 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
...@@ -2197,8 +2236,8 @@ fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() {...@@ -2197,8 +2236,8 @@ fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() {
2197 build(&mut ui);2236 build(&mut ui);
2198 }2237 }
2199 assert_eq!(ui.popups[0].highlight, picker.then_some(30));2238 assert_eq!(ui.popups[0].highlight, picker.then_some(30));
2200 let top = ui.rect(rows).unwrap()[1];2239 let top = ui.laid_out(rows).unwrap()[1];
2201 let first = ui.rect(rows.child(0u64)).map(|rect| rect[1]);2240 let first = ui.laid_out(rows.child(0u64)).map(|rect| rect[1]);
2202 assert_eq!(first == Some(top), !picker, "{first:?} under {top}");2241 assert_eq!(first == Some(top), !picker, "{first:?} under {top}");
2203 }2242 }
2204}2243}
...@@ -2347,7 +2386,8 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {...@@ -2347,7 +2386,8 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {
2347 let fonts = fonts();2386 let fonts = fonts();
2348 let build = |ui: &mut Ui| {2387 let build = |ui: &mut Ui| {
2349 frame(ui, |ui| {2388 frame(ui, |ui| {
2350 popup::menu(ui, menu_id(), BELOW, &fonts, Some("Font"));2389 let anchor = below(ui);
2390 popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"));
2351 })2391 })
2352 };2392 };
2353 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);2393 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
...@@ -2358,7 +2398,7 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {...@@ -2358,7 +2398,7 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {
2358 }2398 }
2359 let results = menu_id().child("rows");2399 let results = menu_id().child("rows");
2360 let height = |ui: &Ui| {2400 let height = |ui: &Ui| {
2361 let rect = ui.rect(results).unwrap();2401 let rect = ui.laid_out(results).unwrap();
2362 rect[3] - rect[1]2402 rect[3] - rect[1]
2363 };2403 };
2364 assert!(2404 assert!(
...@@ -2373,15 +2413,18 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {...@@ -2373,15 +2413,18 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {
2373 for query in ["t", "nr"] {2413 for query in ["t", "nr"] {
2374 ui.event(typed(query));2414 ui.event(typed(query));
2375 build(&mut ui);2415 build(&mut ui);
2376 let shown = [ui.rect(results), ui.rect(results.child(42_u64))];2416 let shown = [ui.laid_out(results), ui.laid_out(results.child(42_u64))];
2377 build(&mut ui);2417 build(&mut ui);
2378 assert!(!ui.wants_frame(), "nothing eases");2418 assert!(!ui.wants_frame(), "nothing eases");
2379 assert_eq!([ui.rect(results), ui.rect(results.child(42_u64))], shown);2419 assert_eq!(
2420 [ui.laid_out(results), ui.laid_out(results.child(42_u64))],
2421 shown
2422 );
2380 }2423 }
2381 let row = ui.rect(results.child(42_u64)).unwrap();2424 let row = ui.laid_out(results.child(42_u64)).unwrap();
2382 assert_eq!(2425 assert_eq!(
2383 row[1],2426 row[1],
2384 ui.rect(results).unwrap()[1],2427 ui.laid_out(results).unwrap()[1],
2385 "Times New Roman at the top"2428 "Times New Roman at the top"
2386 );2429 );
2387 assert_eq!(height(&ui), popup::MENU_ROW);2430 assert_eq!(height(&ui), popup::MENU_ROW);
...@@ -2599,7 +2642,7 @@ fn a_dragged_box_lands_past_the_middles_it_crossed_and_the_rest_slide_aside() {...@@ -2599,7 +2642,7 @@ fn a_dragged_box_lands_past_the_middles_it_crossed_and_the_rest_slide_aside() {
25992642
2600/// Clicks the middle of `id`'s box as laid out now, where it must be hit.2643/// Clicks the middle of `id`'s box as laid out now, where it must be hit.
2601fn click_box(ui: &mut Ui, id: Id) {2644fn click_box(ui: &mut Ui, id: Id) {
2602 let rect = ui.rect(id).unwrap();2645 let rect = ui.laid_out(id).unwrap();
2603 let middle = [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0];2646 let middle = [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0];
2604 assert_eq!(2647 assert_eq!(
2605 ui.box_at(middle),2648 ui.box_at(middle),
...@@ -2662,7 +2705,7 @@ fn dialog_frame(ui: &mut Ui, text: &mut String) -> Reported {...@@ -2662,7 +2705,7 @@ fn dialog_frame(ui: &mut Ui, text: &mut String) -> Reported {
2662 text,2705 text,
2663 ..popup::Item::default()2706 ..popup::Item::default()
2664 });2707 });
2665 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());2708 let anchor = Anchor::Below(combo);
2666 reported.scheme = popup::menu(ui, Id::ROOT.child("schemes"), anchor, &items, None);2709 reported.scheme = popup::menu(ui, Id::ROOT.child("schemes"), anchor, &items, None);
2667 ui.close();2710 ui.close();
2668 });2711 });
...@@ -2722,8 +2765,7 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl...@@ -2722,8 +2765,7 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl
2722 );2765 );
2723 shell::combo(ui, "combo", "Font", "Calibri", 120.0, menu_id(), true);2766 shell::combo(ui, "combo", "Font", "Calibri", 120.0, menu_id(), true);
2724 ui.close();2767 ui.close();
2725 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());2768 chosen = popup::menu(ui, menu_id(), Anchor::Over(combo), &fonts, Some("Font"));
2726 chosen = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"));
2727 });2769 });
2728 chosen2770 chosen
2729 };2771 };
...@@ -2731,12 +2773,12 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl...@@ -2731,12 +2773,12 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl
2731 ui.open_popup(menu_id());2773 ui.open_popup(menu_id());
2732 build(&mut ui);2774 build(&mut ui);
2733 let field = menu_id().child("filter");2775 let field = menu_id().child("filter");
2734 let [box_rect, first] = [ui.rect(combo).unwrap(), ui.rect(field).unwrap()];2776 let [box_rect, first] = [ui.laid_out(combo).unwrap(), ui.laid_out(field).unwrap()];
2735 assert_eq!(first[..2], box_rect[..2], "the field starts on the box");2777 assert_eq!(first[..2], box_rect[..2], "the field starts on the box");
2736 assert!(first[2] - first[0] < box_rect[2] - box_rect[0] + 20.0);2778 assert!(first[2] - first[0] < box_rect[2] - box_rect[0] + 20.0);
2737 let shown = ui.rect(menu_id()).unwrap();2779 let shown = ui.laid_out(menu_id()).unwrap();
2738 let row = menu_id().child("rows").child(3_u64);2780 let row = menu_id().child("rows").child(3_u64);
2739 let opening = ui.rect(row).unwrap();2781 let opening = ui.laid_out(row).unwrap();
2740 assert!(2782 assert!(
2741 opening[2] > shown[2],2783 opening[2] > shown[2],
2742 "the rows lie out past the popup as it opens"2784 "the rows lie out past the popup as it opens"
...@@ -2770,9 +2812,9 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl...@@ -2770,9 +2812,9 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl
2770 build(&mut ui);2812 build(&mut ui);
2771 }2813 }
2772 assert!(!ui.wants_frame());2814 assert!(!ui.wants_frame());
2773 let [panel, last] = [ui.rect(menu_id()).unwrap(), ui.rect(field).unwrap()];2815 let [panel, last] = [ui.laid_out(menu_id()).unwrap(), ui.laid_out(field).unwrap()];
2774 assert_eq!(last[2], panel[2] - 4.0, "the field widens across the popup");2816 assert_eq!(last[2], panel[2] - 4.0, "the field widens across the popup");
2775 assert_eq!(ui.rect(row), Some(opening), "the rows never move");2817 assert_eq!(ui.laid_out(row), Some(opening), "the rows never move");
2776 click_box(&mut ui, menu_id().child("rows").child(3_u64));2818 click_box(&mut ui, menu_id().child("rows").child(3_u64));
2777 assert_eq!(build(&mut ui), Some(3));2819 assert_eq!(build(&mut ui), Some(3));
2778}2820}
...@@ -2781,9 +2823,9 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl...@@ -2781,9 +2823,9 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl
2781fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {2823fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
2782 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);2824 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
2783 let fonts = fonts();2825 let fonts = fonts();
2784 let anchor = Anchor::Over([330.0, 10.0, 390.0, 30.0]);
2785 let build = |ui: &mut Ui| {2826 let build = |ui: &mut Ui| {
2786 frame(ui, |ui| {2827 frame(ui, |ui| {
2828 let anchor = Anchor::Over(spot(ui, [330.0, 10.0, 390.0, 30.0]));
2787 _ = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"))2829 _ = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"))
2788 })2830 })
2789 };2831 };
...@@ -2791,11 +2833,12 @@ fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {...@@ -2791,11 +2833,12 @@ fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
2791 ui.open_popup(menu_id());2833 ui.open_popup(menu_id());
2792 build(&mut ui);2834 build(&mut ui);
2793 let field = menu_id().child("filter");2835 let field = menu_id().child("filter");
2794 let edges =2836 let edges = |ui: &Ui| {
2795 |ui: &Ui| [ui.rect(field).unwrap(), ui.rect(menu_id()).unwrap()].map(|rect| rect[2]);2837 [ui.laid_out(field).unwrap(), ui.laid_out(menu_id()).unwrap()].map(|rect| rect[2])
2796 let first = ui.rect(field).unwrap();2838 };
2839 let first = ui.laid_out(field).unwrap();
2797 let row = menu_id().child("rows").child(3_u64);2840 let row = menu_id().child("rows").child(3_u64);
2798 let opening = ui.rect(row).unwrap();2841 let opening = ui.laid_out(row).unwrap();
2799 assert_eq!(2842 assert_eq!(
2800 edges(&ui),2843 edges(&ui),
2801 [390.0, 394.0],2844 [390.0, 394.0],
...@@ -2807,10 +2850,10 @@ fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {...@@ -2807,10 +2850,10 @@ fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
2807 }2850 }
2808 assert_eq!(edges(&ui), [390.0, 394.0], "the trailing edges hold");2851 assert_eq!(edges(&ui), [390.0, 394.0], "the trailing edges hold");
2809 assert!(2852 assert!(
2810 ui.rect(field).unwrap()[0] < first[0] - 30.0,2853 ui.laid_out(field).unwrap()[0] < first[0] - 30.0,
2811 "the field widens leftward"2854 "the field widens leftward"
2812 );2855 );
2813 assert_eq!(ui.rect(row), Some(opening), "the rows never move");2856 assert_eq!(ui.laid_out(row), Some(opening), "the rows never move");
2814}2857}
28152858
2816fn built(ui: &Ui, id: Id) -> &Built {2859fn built(ui: &Ui, id: Id) -> &Built {
...@@ -2835,7 +2878,8 @@ fn split_frame(ui: &mut Ui) -> Signal {...@@ -2835,7 +2878,8 @@ fn split_frame(ui: &mut Ui) -> Signal {
2835 text: "Item",2878 text: "Item",
2836 ..Default::default()2879 ..Default::default()
2837 };2880 };
2838 popup::menu(ui, split_menu(), BELOW, &[item], None);2881 let anchor = below(ui);
2882 popup::menu(ui, split_menu(), anchor, &[item], None);
2839 ui.close();2883 ui.close();
2840 });2884 });
2841 signal2885 signal
...@@ -2867,24 +2911,24 @@ fn a_split_button_fills_its_button_alone_and_outlines_both_halves_from_its_arrow...@@ -2867,24 +2911,24 @@ fn a_split_button_fills_its_button_alone_and_outlines_both_halves_from_its_arrow
2867 split_frame(ui);2911 split_frame(ui);
2868 }2912 }
2869 };2913 };
2870 ui.event(Event::PointerMoved(center(ui.rect(button).unwrap())));2914 ui.event(Event::PointerMoved(center(ui.laid_out(button).unwrap())));
2871 settle(&mut ui);2915 settle(&mut ui);
2872 // The fill rounds past the button's clip, so it meets the arrow square.2916 // The fill rounds past the button's clip, so it meets the arrow square.
2873 let [left, top, right, bottom] = ui.rect(button).unwrap();2917 let [left, top, right, bottom] = ui.laid_out(button).unwrap();
2874 assert!(built(&ui, face).fill.is_some());2918 assert!(built(&ui, face).fill.is_some());
2875 assert_eq!(ui.rect(face), Some([left, top, right + 4.0, bottom]));2919 assert_eq!(ui.laid_out(face), Some([left, top, right + 4.0, bottom]));
2876 assert!(built(&ui, button).flags.contains(Flags::CLIP));2920 assert!(built(&ui, button).flags.contains(Flags::CLIP));
2877 assert!(built(&ui, ring).border.is_none());2921 assert!(built(&ui, ring).border.is_none());
2878 ui.event(Event::PointerMoved(center(ui.rect(arrow).unwrap())));2922 ui.event(Event::PointerMoved(center(ui.laid_out(arrow).unwrap())));
2879 settle(&mut ui);2923 settle(&mut ui);
2880 assert!(built(&ui, face).fill.is_none());2924 assert!(built(&ui, face).fill.is_none());
2881 assert!(built(&ui, ring).border.is_some());2925 assert!(built(&ui, ring).border.is_some());
2882 assert_eq!(2926 assert_eq!(
2883 ui.rect(ring),2927 ui.laid_out(ring),
2884 Some([left, top, ui.rect(arrow).unwrap()[2], bottom])2928 Some([left, top, ui.laid_out(arrow).unwrap()[2], bottom])
2885 );2929 );
2886 // The arrow opens the menu, and the outline stays while it is open.2930 // The arrow opens the menu, and the outline stays while it is open.
2887 let point = center(ui.rect(arrow).unwrap());2931 let point = center(ui.laid_out(arrow).unwrap());
2888 click(&mut ui, point);2932 click(&mut ui, point);
2889 assert!(!split_frame(&mut ui).clicked);2933 assert!(!split_frame(&mut ui).clicked);
2890 assert!(ui.popup_open(split_menu()));2934 assert!(ui.popup_open(split_menu()));
...@@ -2893,7 +2937,7 @@ fn a_split_button_fills_its_button_alone_and_outlines_both_halves_from_its_arrow...@@ -2893,7 +2937,7 @@ fn a_split_button_fills_its_button_alone_and_outlines_both_halves_from_its_arrow
2893 assert!(built(&ui, ring).border.is_some());2937 assert!(built(&ui, ring).border.is_some());
2894 ui.close_popup(split_menu());2938 ui.close_popup(split_menu());
2895 settle(&mut ui);2939 settle(&mut ui);
2896 let point = center(ui.rect(button).unwrap());2940 let point = center(ui.laid_out(button).unwrap());
2897 click(&mut ui, point);2941 click(&mut ui, point);
2898 assert!(split_frame(&mut ui).clicked);2942 assert!(split_frame(&mut ui).clicked);
2899 assert!(!ui.popup_open(split_menu()));2943 assert!(!ui.popup_open(split_menu()));
...@@ -2932,7 +2976,7 @@ fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {...@@ -2932,7 +2976,7 @@ fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {
2932 });2976 });
2933 };2977 };
2934 build(&mut ui);2978 build(&mut ui);
2935 let button = ui.rect(Id::ROOT.child("bar").child("align")).unwrap();2979 let button = ui.laid_out(Id::ROOT.child("bar").child("align")).unwrap();
2936 // One target: the arrow opens the menu as the icon does.2980 // One target: the arrow opens the menu as the icon does.
2937 click(&mut ui, [button[2] - 3.0, center(button)[1]]);2981 click(&mut ui, [button[2] - 3.0, center(button)[1]]);
2938 for _ in 0..40 {2982 for _ in 0..40 {
...@@ -2940,7 +2984,7 @@ fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {...@@ -2940,7 +2984,7 @@ fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {
2940 }2984 }
2941 assert!(ui.popup_open(menu_id()));2985 assert!(ui.popup_open(menu_id()));
2942 let icon = ui2986 let icon = ui
2943 .rect(2987 .laid_out(
2944 menu_id()2988 menu_id()
2945 .child("rows")2989 .child("rows")
2946 .child(0_usize)2990 .child(0_usize)
...@@ -2965,7 +3009,8 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() {...@@ -2965,7 +3009,8 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() {
2965 let build = |ui: &mut Ui| {3009 let build = |ui: &mut Ui| {
2966 let mut chosen = None;3010 let mut chosen = None;
2967 frame(ui, |ui| {3011 frame(ui, |ui| {
2968 chosen = popup::gallery(ui, gallery, BELOW, &groups, &[3], |ui, index| {3012 let anchor = below(ui);
3013 chosen = popup::gallery(ui, gallery, anchor, &groups, &[3], |ui, index| {
2969 ui.leaf(3014 ui.leaf(
2970 "label",3015 "label",
2971 Spec {3016 Spec {
...@@ -2990,7 +3035,7 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() {...@@ -2990,7 +3035,7 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() {
2990 for _ in 0..40 {3035 for _ in 0..40 {
2991 build(&mut ui);3036 build(&mut ui);
2992 }3037 }
2993 let cell = |index: usize| ui.rect(gallery.child(("cell", index))).unwrap();3038 let cell = |index: usize| ui.laid_out(gallery.child(("cell", index))).unwrap();
2994 // The library starts a row of its own under its heading.3039 // The library starts a row of its own under its heading.
2995 assert_eq!(cell(2)[0], cell(0)[0]);3040 assert_eq!(cell(2)[0], cell(0)[0]);
2996 assert!(cell(2)[1] > cell(0)[3]);3041 assert!(cell(2)[1] > cell(0)[3]);
...@@ -3007,7 +3052,7 @@ fn desktop_menus_cut_or_fade_without_growing() {...@@ -3007,7 +3052,7 @@ fn desktop_menus_cut_or_fade_without_growing() {
3007 Layer::Custom { .. } => None,3052 Layer::Custom { .. } => None,
3008 })3053 })
3009 };3054 };
3010 let at = Anchor::Point([50.0, 50.0]);3055 let at = |_: &mut Ui| Anchor::Point([50.0, 50.0]);
3011 let styled = |motion| {3056 let styled = |motion| {
3012 let mut theme = Theme::light();3057 let mut theme = Theme::light();
3013 theme.desktop_menu = Some(Menu {3058 theme.desktop_menu = Some(Menu {
...@@ -3066,7 +3111,9 @@ fn tab_row(ui: &mut Ui, active: usize) -> Id {...@@ -3066,7 +3111,9 @@ fn tab_row(ui: &mut Ui, active: usize) -> Id {
3066 ..Spec::default()3111 ..Spec::default()
3067 },3112 },
3068 );3113 );
3069 shell::section_tabs(ui, row, &tabs, active, None, None, &section, 28.0, [0.0; 4]);3114 shell::section_tabs(
3115 ui, row, &tabs, active, None, None, None, &section, 28.0, [0.0; 4],
3116 );
3070 ui.close();3117 ui.close();
3071 });3118 });
3072 row3119 row
...@@ -3085,10 +3132,10 @@ fn settle_tabs(ui: &mut Ui, active: usize) -> Id {...@@ -3085,10 +3132,10 @@ fn settle_tabs(ui: &mut Ui, active: usize) -> Id {
3085fn overflowing_tabs_scroll_the_open_one_into_view() {3132fn overflowing_tabs_scroll_the_open_one_into_view() {
3086 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);3133 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3087 let row = settle_tabs(&mut ui, 0);3134 let row = settle_tabs(&mut ui, 0);
3088 let view = ui.rect(row).unwrap();3135 let view = ui.laid_out(row).unwrap();
3089 assert_eq!(view[2] - view[0], 200.0);3136 assert_eq!(view[2] - view[0], 200.0);
3090 let row = settle_tabs(&mut ui, 11);3137 let row = settle_tabs(&mut ui, 11);
3091 let last = ui.rect(shell::tab_id(row, 11)).unwrap();3138 let last = ui.laid_out(shell::tab_id(row, 11)).unwrap();
3092 assert!(3139 assert!(
3093 last[0] >= view[0] && last[2] <= view[2],3140 last[0] >= view[0] && last[2] <= view[2],
3094 "{last:?} in {view:?}"3141 "{last:?} in {view:?}"
...@@ -3099,17 +3146,17 @@ fn overflowing_tabs_scroll_the_open_one_into_view() {...@@ -3099,17 +3146,17 @@ fn overflowing_tabs_scroll_the_open_one_into_view() {
3099fn the_wheel_scrolls_overflowing_tabs_sideways_either_way_it_turns() {3146fn the_wheel_scrolls_overflowing_tabs_sideways_either_way_it_turns() {
3100 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);3147 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3101 let row = settle_tabs(&mut ui, 0);3148 let row = settle_tabs(&mut ui, 0);
3102 let first = ui.rect(shell::tab_id(row, 0)).unwrap()[0];3149 let first = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3103 ui.event(Event::PointerMoved([100.0, 14.0]));3150 ui.event(Event::PointerMoved([100.0, 14.0]));
3104 ui.event(Event::Wheel([0.0, -40.0]));3151 ui.event(Event::Wheel([0.0, -40.0]));
3105 tab_row(&mut ui, 0);3152 tab_row(&mut ui, 0);
3106 tab_row(&mut ui, 0);3153 tab_row(&mut ui, 0);
3107 let scrolled = ui.rect(shell::tab_id(row, 0)).unwrap()[0];3154 let scrolled = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3108 assert_eq!(first - scrolled, 40.0, "at once, as on the page");3155 assert_eq!(first - scrolled, 40.0, "at once, as on the page");
3109 ui.event(Event::Wheel([30.0, 0.0]));3156 ui.event(Event::Wheel([30.0, 0.0]));
3110 tab_row(&mut ui, 0);3157 tab_row(&mut ui, 0);
3111 tab_row(&mut ui, 0);3158 tab_row(&mut ui, 0);
3112 let back = ui.rect(shell::tab_id(row, 0)).unwrap()[0];3159 let back = ui.laid_out(shell::tab_id(row, 0)).unwrap()[0];
3113 assert_eq!(back - scrolled, 30.0);3160 assert_eq!(back - scrolled, 30.0);
3114}3161}
31153162
...@@ -3120,10 +3167,11 @@ fn a_colour_picker_applies_its_colour_and_keeps_it_through_hsl() {...@@ -3120,10 +3167,11 @@ fn a_colour_picker_applies_its_colour_and_keeps_it_through_hsl() {
3120 let build = |ui: &mut Ui| {3167 let build = |ui: &mut Ui| {
3121 let mut chosen = None;3168 let mut chosen = None;
3122 frame(ui, |ui| {3169 frame(ui, |ui| {
3170 let anchor = below(ui);
3123 chosen = popup::color_picker(3171 chosen = popup::color_picker(
3124 ui,3172 ui,
3125 picker,3173 picker,
3126 BELOW,3174 anchor,
3127 "Custom Color",3175 "Custom Color",
3128 [0xd4, 0xf9, 0xf2],3176 [0xd4, 0xf9, 0xf2],
3129 |_| [1.0; 4],3177 |_| [1.0; 4],
...@@ -3246,7 +3294,7 @@ fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_...@@ -3246,7 +3294,7 @@ fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_
3246 ui.close();3294 ui.close();
3247 });3295 });
3248 };3296 };
3249 let height = |ui: &Ui, id: Id| ui.rect(id).map(|rect| rect[3] - rect[1]).unwrap();3297 let height = |ui: &Ui, id: Id| ui.laid_out(id).map(|rect| rect[3] - rect[1]).unwrap();
3250 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);3298 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
3251 build(&mut ui, 2);3299 build(&mut ui, 2);
3252 ui.open_popup(dialog);3300 ui.open_popup(dialog);
...@@ -3271,7 +3319,7 @@ fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_...@@ -3271,7 +3319,7 @@ fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_
3271 for _ in 0..60 {3319 for _ in 0..60 {
3272 build(&mut ui, 20);3320 build(&mut ui, 20);
3273 }3321 }
3274 let [top, row] = [list, list.child(10_usize)].map(|id| ui.rect(id).unwrap()[1]);3322 let [top, row] = [list, list.child(10_usize)].map(|id| ui.laid_out(id).unwrap()[1]);
3275 assert!(3323 assert!(
3276 (row - top).abs() < 0.5,3324 (row - top).abs() < 0.5,
3277 "row 10 at {row}, the list's top at {top}"3325 "row 10 at {row}, the list's top at {top}"
...@@ -3316,7 +3364,7 @@ fn a_box_has_no_rectangle_until_it_is_first_laid_out() {...@@ -3316,7 +3364,7 @@ fn a_box_has_no_rectangle_until_it_is_first_laid_out() {
3316 ..Spec::default()3364 ..Spec::default()
3317 },3365 },
3318 );3366 );
3319 ui.rect(Id::ROOT.child("new"))3367 ui.laid_out(Id::ROOT.child("new"))
3320 };3368 };
3321 let mut seen = Vec::new();3369 let mut seen = Vec::new();
3322 frame(&mut ui, |ui| seen.push(build(ui)));3370 frame(&mut ui, |ui| seen.push(build(ui)));
crates/ui/src/widgets.rs+3-3
...@@ -360,7 +360,7 @@ pub fn scrollbar(...@@ -360,7 +360,7 @@ pub fn scrollbar(
360 return system_scrollbar(ui, part, axis, offset, range, view, corner);360 return system_scrollbar(ui, part, axis, offset, range, view, corner);
361 }361 }
362 let along = usize::from(axis == Axis::Y);362 let along = usize::from(axis == Axis::Y);
363 let rect = ui.rect(ui.current())?;363 let rect = ui.laid_out(ui.current())?;
364 let length = rect[along + 2] - rect[along];364 let length = rect[along + 2] - rect[along];
365 let cross = rect[3 - along] - rect[1 - along];365 let cross = rect[3 - along] - rect[1 - along];
366 let span = range[1] - range[0];366 let span = range[1] - range[0];
...@@ -415,7 +415,7 @@ fn system_scrollbar(...@@ -415,7 +415,7 @@ fn system_scrollbar(
415 corner: bool,415 corner: bool,
416) -> Option<f32> {416) -> Option<f32> {
417 let along = usize::from(axis == Axis::Y);417 let along = usize::from(axis == Axis::Y);
418 let rect = ui.rect(ui.current())?;418 let rect = ui.laid_out(ui.current())?;
419 let span = range[1] - range[0];419 let span = range[1] - range[0];
420 let thickness = ui.scrollers.as_ref()?.thickness;420 let thickness = ui.scrollers.as_ref()?.thickness;
421 let length = rect[along + 2] - rect[along] - if corner { thickness } else { 0.0 };421 let length = rect[along + 2] - rect[along] - if corner { thickness } else { 0.0 };
...@@ -572,7 +572,7 @@ fn field(...@@ -572,7 +572,7 @@ fn field(
572 let modifiers = edit_modifiers(ui.modifiers());572 let modifiers = edit_modifiers(ui.modifiers());
573 let pointer = ui573 let pointer = ui
574 .pointer()574 .pointer()
575 .zip(ui.rect(id))575 .zip(ui.laid_out(id))
576 .map(|(pointer, rect)| pointer[0] - rect[0] - pad)576 .map(|(pointer, rect)| pointer[0] - rect[0] - pad)
577 .filter(|_| signal.pressed || signal.dragging);577 .filter(|_| signal.pressed || signal.dragging);
578 let state = ui.state(id);578 let state = ui.state(id);