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(
8686 queue: &wgpu::Queue,
8787) -> Vec<u8> {
8888 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();
9090 let box_size = [
9191 ((corner[2] - corner[0]) * SCALE) as u32,
9292 ((corner[3] - corner[1]) * SCALE) as u32,
......@@ -281,7 +281,7 @@ fn conflict_views_offer_the_versions_and_render_offscreen() {
281281 other => panic!("{other:?}", other = other.0),
282282 }
283283 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();
285285 assert_eq!(listed, shown.is_some(), "{name}");
286286 if let (Some(output), Some((renderer, device, queue))) = (&output, &mut gpu) {
287287 let shown = session.reader(space)().unwrap();
......@@ -398,7 +398,7 @@ fn page_versions_list_open_read_only_and_render_offscreen() {
398398 }
399399 for context in &versions {
400400 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}");
402402 }
403403 if let (Some(output), Some((renderer, device, queue))) = (&output, &mut gpu) {
404404 let shown = match version {
crates/snowbound/src/live.rs+4-7
......@@ -676,11 +676,8 @@ impl State {
676676 ..ui::popup::Item::default()
677677 })
678678 .collect();
679 let rect = self.ui.rect(self.ui.id("more"));
680 if let Some(rect) = rect
681 && let Some(index) =
682 ui::popup::menu(&mut self.ui, menu, ui::Anchor::Below(rect), &items, None)
683 {
679 let anchor = ui::Anchor::Below(self.ui.id("more"));
680 if let Some(index) = ui::popup::menu(&mut self.ui, menu, anchor, &items, None) {
684681 chosen = Some(peers[index].hello.peer);
685682 }
686683 }
......@@ -874,7 +871,7 @@ impl State {
874871 let Some(session) = &self.session else {
875872 return;
876873 };
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 {
878875 return;
879876 };
880877 let here = (section(session), Some(session.space.into()));
......@@ -998,7 +995,7 @@ impl State {
998995 [x0, y0, y1]: [f32; 3],
999996 selection: &[[f32; 4]],
1000997 ) {
1001 let Some([left, top, ..]) = self.ui.rect(page()) else {
998 let Some([left, top, ..]) = self.ui.laid_out(page()) else {
1002999 return;
10031000 };
10041001 let id = peer.hello.peer;
crates/snowbound/src/main.rs+93-58
......@@ -1602,7 +1602,7 @@ impl State {
16021602 |_| [0.0; 4],
16031603 );
16041604 }
1605 if let Some(rect) = self.ui.rect(page()) {
1605 if let Some(rect) = self.ui.laid_out(page()) {
16061606 let size = [
16071607 ((rect[2] - rect[0]) * scale).round() as u32,
16081608 ((rect[3] - rect[1]) * scale).round() as u32,
......@@ -1712,7 +1712,7 @@ impl State {
17121712 .ui
17131713 .animate(tab_row(), if self.full_page { 0.0 } else { TAB_ROW });
17141714 let drags = self.chrome_drags();
1715 let tab_row = self.ui.open_as(
1715 self.ui.open_as(
17161716 tab_row(),
17171717 Spec {
17181718 flags: if drags {
......@@ -1757,24 +1757,41 @@ impl State {
17571757 let unread = self.unread_keys();
17581758 let (clicked, open_tab) = match &self.session {
17591759 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);
17601763 // 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()));
17611772 let tabs: Vec<_> = session
17621773 .tabs
17631774 .iter()
1764 .map(|tab| {
1765 let name = match &self.renaming {
1766 Some(renaming) if renaming.entry(&session.library, &tab.path, true) => {
1767 renaming.name.as_str()
1768 }
1775 .enumerate()
1776 .map(|(index, tab)| {
1777 let name = match &typed {
1778 Some((renamed, name)) if *renamed == index => name.as_str(),
17691779 _ => tab.name.as_str(),
17701780 };
17711781 let unread = unread.contains(&session.library.key(&tab.path));
17721782 (name, section_color(tab.color), unread)
17731783 })
17741784 .collect();
1775 let lit = self.page_drop(row);
1776 let dragged = self.dragged_tab(row);
1777 let shown = self.open_tab().unwrap_or(session.tab);
1785 let mut kept = None;
1786 let mut field;
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 };
17781795 let ui::shell::Tabs {
17791796 clicked,
17801797 context,
......@@ -1790,10 +1807,14 @@ impl State {
17901807 shown,
17911808 lit,
17921809 dragged,
1810 renaming,
17931811 &section,
17941812 TAB_ROW,
17951813 theme.strip,
17961814 );
1815 if let Some(keep) = kept {
1816 self.finish_renaming(keep);
1817 }
17971818 name(&mut self.ui, row, "Sections");
17981819 self.drag_tabs(held, slot, settled, row);
17991820 let Some(session) = &self.session else {
......@@ -1825,7 +1846,6 @@ impl State {
18251846 };
18261847 self.rename(target);
18271848 }
1828 self.tab_rename_field(&theme, row, tab_row);
18291849 (clicked, open_tab)
18301850 }
18311851 None if self.locked.is_some() => self.locked_tabs(row, &section, theme.strip),
......@@ -1839,6 +1859,7 @@ impl State {
18391859 0,
18401860 None,
18411861 None,
1862 None,
18421863 &section,
18431864 TAB_ROW,
18441865 theme.strip,
......@@ -1963,9 +1984,7 @@ impl State {
19631984 if !opening {
19641985 self.template_strip(&theme);
19651986 }
1966 if let Some(rect) = self.ui.rect(page()) {
1967 self.transport(&theme, rect)?;
1968 }
1987 self.transport(&theme)?;
19691988 let scroll = self.view.scroll();
19701989 for (index, axis) in [Axis::X, Axis::Y]
19711990 .into_iter()
......@@ -1990,20 +2009,24 @@ impl State {
19902009 if !opening {
19912010 self.peer_carets();
19922011 }
1993 self.ui.close();
1994 if let Some(task) = self.view.task_under_pointer() {
1995 let [left, top, ..] = self.ui.rect(page()).unwrap_or_default();
2012 let task = self.view.task_under_pointer().map(|[x0, y0, x1, y1]| {
19962013 let scale = self.ui.scale();
1997 let [x0, y0, x1, y1] = task;
1998 let part = [
1999 left + x0 / scale,
2000 top + y0 / scale,
2001 left + x1 / scale,
2002 top + y1 / scale,
2003 ];
2014 self.ui.leaf(
2015 "task",
2016 Spec {
2017 flags: Flags::FLOAT,
2018 size: [px((x1 - x0) / scale), px((y1 - y0) / scale)],
2019 position: [x0 / scale, y0 / scale],
2020 ..Spec::default()
2021 },
2022 );
2023 page().child("task")
2024 });
2025 self.ui.close();
2026 if let Some(task) = task {
20042027 ui::popup::tooltip_over(
20052028 &mut self.ui,
2006 part,
2029 task,
20072030 "Outlook task",
20082031 Some("Edit it in OneNote with Outlook"),
20092032 );
......@@ -2046,8 +2069,9 @@ impl State {
20462069 /// Borders the open section tab and the frame's top, and rounds and borders the page
20472070 /// together with the open page's tab, joined where they meet.
20482071 fn edges(&mut self, section: ui::Section, open_tab: Id, open_page: Option<Id>) {
2049 let panel = self.ui.rect(frame().child("panel"));
2050 let (Some(frame), Some(page)) = (self.ui.rect(frame()), self.ui.rect(page())) else {
2072 let panel = self.ui.laid_out(frame().child("panel"));
2073 let (Some(frame), Some(page)) = (self.ui.laid_out(frame()), self.ui.laid_out(page()))
2074 else {
20512075 return;
20522076 };
20532077 let rounding = self.rounding();
......@@ -2056,7 +2080,7 @@ impl State {
20562080 let [left, top, right, bottom] = page;
20572081 let left_tabs = self.page_tabs_left;
20582082 let tab = open_page
2059 .and_then(|id| self.ui.rect(id))
2083 .and_then(|id| self.ui.laid_out(id))
20602084 .map(|row| [row[0], row[1], row[2], row[3] - ROW_GAP])
20612085 .zip(panel)
20622086 .and_then(|(row, panel)| {
......@@ -2129,7 +2153,7 @@ impl State {
21292153 // the open tab stands on it.
21302154 let [start, end] = [frame[0], frame[2]];
21312155 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];
21332157 let [beside, beside_right] =
21342158 [start, window - end].map(|gap| (outer * gap / sidebar::WIDTH).min(outer));
21352159 let strip = self.ui.theme.strip;
......@@ -2153,11 +2177,11 @@ impl State {
21532177 // The border breaks where the open tab shows, which the row may scroll it out of.
21542178 let [left, right] = self
21552179 .ui
2156 .rect(sections())
2180 .laid_out(sections())
21572181 .map_or([end; 2], |row| [row[0], row[2]]);
21582182 let [foot, toe] = self
21592183 .ui
2160 .rect(open_tab)
2184 .laid_out(open_tab)
21612185 .map_or([end; 2], |tab| ui::shell::tab_base(tab, TAB_ROW))
21622186 .map(|x| x.clamp(left, right));
21632187 // The border runs down both sides to the window's bottom, round the bottom corner
......@@ -2398,7 +2422,7 @@ impl State {
23982422 disabled: !enabled,
23992423 ..Default::default()
24002424 }));
2401 let anchor = ui::Anchor::Below(ui.rect(ui.id("menu")).unwrap_or_default());
2425 let anchor = ui::Anchor::Below(ui.id("menu"));
24022426 if ui::popup::menu(ui, toolbar_popup("paste"), anchor, &items, None).is_some() {
24032427 choice = Some(Choice::Command(Cmd::Paste));
24042428 }
......@@ -2422,7 +2446,7 @@ impl State {
24222446 ui::shell::menu_button(ui, "styles", art::STYLES, None, styles)
24232447 } else {
24242448 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"))
24262450 };
24272451 let shown = notebook::sidecar::themes::STYLES
24282452 .iter()
......@@ -2485,7 +2509,7 @@ impl State {
24852509 ..Default::default()
24862510 })
24872511 .collect();
2488 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());
2512 let anchor = ui::Anchor::Over(combo);
24892513 if let Some(index) =
24902514 ui::popup::menu(ui, toolbar_popup("font"), anchor, &items, Some("Font"))
24912515 && let Some(name) = choices[index].1
......@@ -2534,7 +2558,7 @@ impl State {
25342558 ..Default::default()
25352559 });
25362560 }
2537 let anchor = ui::Anchor::Over(ui.rect(combo).unwrap_or_default());
2561 let anchor = ui::Anchor::Over(combo);
25382562 if let Some(index) = ui::popup::menu(ui, toolbar_popup("size"), anchor, &items, Some(&size))
25392563 {
25402564 choice = Some(Choice::Size(sizes[index]));
......@@ -2601,7 +2625,7 @@ impl State {
26012625 .iter()
26022626 .map(|&(color, name)| (colorref(color), name))
26032627 .collect();
2604 let anchor = ui::Anchor::Below(ui.rect(split).unwrap_or_default());
2628 let anchor = ui::Anchor::Below(split);
26052629 if let Some(chosen) =
26062630 ui::popup::colors(ui, toolbar_popup(part), anchor, none, &colors, columns)
26072631 {
......@@ -2681,8 +2705,7 @@ impl State {
26812705 }
26822706 tip(ui, id);
26832707 }
2684 let [bullets, numbering] =
2685 ["bullets", "numbering"].map(|part| ui.rect(ui.id(part)).unwrap_or_default());
2708 let [bullets, numbering] = ["bullets", "numbering"].map(|part| ui.id(part));
26862709 let current = |bullet| match state.list {
26872710 Some(ListStyle::Bullet(place)) if bullet => Some(place),
26882711 Some(ListStyle::Number(place)) if !bullet => Some(place),
......@@ -2982,7 +3005,7 @@ impl State {
29823005 choice = Some(Choice::Command(Cmd::Pen));
29833006 }
29843007 tip(ui, Cmd::Pen);
2985 let anchor = ui::Anchor::Below(ui.rect(split).unwrap_or_default());
3008 let anchor = ui::Anchor::Below(split);
29863009 if let Some(place) = pen_gallery(ui, anchor, &drawing_pens, pens.pen) {
29873010 choice = Some(Choice::Pen(place));
29883011 }
......@@ -3367,7 +3390,7 @@ impl State {
33673390 }
33683391 let read_only = self.session.as_ref().is_some_and(Session::read_only);
33693392 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();
33713394 let device = |point: [f32; 2]| {
33723395 [
33733396 (point[0] - corner[0]) * scale,
......@@ -3934,7 +3957,7 @@ impl State {
39343957 /// Follows the view moving: the input method's position and accessibility.
39353958 fn after_move(&mut self) -> Result<(), Box<dyn Error>> {
39363959 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();
39383961 let [x0, y0, x1, y1] = self.view.caret_area()?;
39393962 self.window.set_ime_cursor_area(
39403963 LogicalPosition::new(x0 / scale + corner[0], y0 / scale + corner[1]),
......@@ -4292,7 +4315,7 @@ impl State {
42924315 /// `page` asks or its tree is not grafted yet. The interface's tree goes first, since it
42934316 /// holds the page's.
42944317 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();
42964319 if !built && self.page_grafted {
42974320 // Its graft is gone, and with it the tree, which must be sent whole again.
42984321 self.accessibility.deactivate();
......@@ -4396,12 +4419,12 @@ impl State {
43964419 .accessibility_tree(&self.window.title(), self.window.scale_factor());
43974420 let focus = std::mem::replace(&mut chrome.focus, ui::Id::ROOT.node());
43984421 let mut tree = Tree::new(chrome, true);
4399 if self.ui.rect(page()).is_some() {
4422 if self.ui.laid_out(page()).is_some() {
44004423 let mut fresh = accessibility::Accessibility::default();
44014424 let page = page_tree(&self.view, &self.ui, &self.window, &mut fresh)?;
44024425 tree.update_and_process_changes(page, &mut Unwatched);
44034426 }
4404 if focus != graft || self.ui.rect(page()).is_some() {
4427 if focus != graft || self.ui.laid_out(page()).is_some() {
44054428 let update = accesskit::TreeUpdate {
44064429 nodes: Vec::new(),
44074430 tree: None,
......@@ -4567,7 +4590,7 @@ impl State {
45674590 }
45684591 let theme = &self.ui.theme;
45694592 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();
45714594 // A page taking a while to open shows its outline, drawn in points from the corner.
45724595 let (page_primitives, viewport) = match self.loading() {
45734596 Some(since) => (
......@@ -4640,14 +4663,14 @@ impl State {
46404663 }
46414664
46424665 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();
46444667 let [x, y] = self.pointer;
46454668 (x0..x1).contains(&x) && (y0..y1).contains(&y)
46464669 }
46474670
46484671 /// Window point `point` in the page's device pixels.
46494672 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();
46514674 let scale = self.ui.scale();
46524675 [(point[0] - left) * scale, (point[1] - top) * scale]
46534676 }
......@@ -4777,7 +4800,7 @@ fn page_tree(
47774800 window: &Window,
47784801 access: &mut accessibility::Accessibility,
47794802) -> 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();
47814804 let scale = ui.scale();
47824805 let ratio = window.scale_factor() as f32 / scale;
47834806 let viewport = canvas::gpu::Viewport {
......@@ -4868,8 +4891,8 @@ fn conflict_bar(ui: &mut Ui, bar: Bar, sections: &[&str], steps: [bool; 2]) -> O
48684891 ..Spec::default()
48694892 },
48704893 );
4871 for (part, text) in [("said", said), ("action", action)] {
4872 ui.leaf(
4894 let line = |ui: &mut Ui, part: &str, text| {
4895 ui.open(
48734896 part,
48744897 Spec {
48754898 size: [fill(), fit()],
......@@ -4879,16 +4902,28 @@ fn conflict_bar(ui: &mut Ui, bar: Bar, sections: &[&str], steps: [bool; 2]) -> O
48794902 ..Spec::default()
48804903 },
48814904 );
4882 }
4883 let action = ui.id("action");
4905 };
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();
48844921 ui.close();
48854922 ui.close();
48864923 let clicked = ui.signal(row).clicked;
48874924 let menu = Id::ROOT.child("conflict-menu");
48884925 let copy = Id::ROOT.child("conflict-copy");
4889 // The menu opens under the line the click asks for, as wide as its items.
4890 let [left, top, _, bottom] = ui.rect(action).unwrap_or_default();
4891 let anchor = ui::Anchor::Below([left, top, left, bottom]);
4926 let anchor = ui::Anchor::Below(from);
48924927 match bar {
48934928 Bar::Page { page, shown } => clicked.then_some(Command::Versions { page, show: !shown }),
48944929 Bar::History {
......@@ -5568,7 +5603,7 @@ fn dropdown(
55685603 choice = Some(Choice::Command(id));
55695604 }
55705605 tip(ui, id);
5571 ui::Anchor::Below(ui.rect(button).unwrap_or_default())
5606 ui::Anchor::Below(button)
55725607 }
55735608 Head::Tag(place, tag) if status_of(commands::Id::Tag(place)).enabled => {
55745609 let on = status_of(commands::Id::Tag(place)).checked;
......@@ -5577,7 +5612,7 @@ fn dropdown(
55775612 choice = Some(Choice::Command(commands::Id::Tag(place)));
55785613 }
55795614 tag_tip(ui, place, tag);
5580 ui::Anchor::Below(ui.rect(button).unwrap_or_default())
5615 ui::Anchor::Below(button)
55815616 }
55825617 // Where its command does not apply, the button only opens the menu.
55835618 Head::Split(id) => {
......@@ -5596,7 +5631,7 @@ fn dropdown(
55965631 let tint = ui.theme.text;
55975632 ui::shell::unavailable(ui, part, icon, tint, true);
55985633 name(ui, button, label);
5599 ui::Anchor::Below(ui.rect(button).unwrap_or_default())
5634 ui::Anchor::Below(button)
56005635 }
56015636 Head::Menu(label, icon) => {
56025637 let anchor = ui::shell::menu_button(ui, part, icon, None, menu);
crates/snowbound/src/menus.rs+8-8
......@@ -406,7 +406,7 @@ impl State {
406406 shown
407407 && self
408408 .ui
409 .rect(crate::sections())
409 .laid_out(crate::sections())
410410 .is_some_and(|[left, top, right, bottom]| {
411411 x >= left && x < right && y >= top && y < bottom
412412 })
......@@ -1108,7 +1108,7 @@ impl State {
11081108 (0..session.tabs.len())
11091109 .filter(|tab| *tab != session.tab)
11101110 .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(
11121112 |[left, top, right, bottom]| x >= left && x < right && y >= top && y < bottom,
11131113 )
11141114 })
......@@ -1120,7 +1120,7 @@ impl State {
11201120 let Dragged::Page(space) = drag.what else {
11211121 return None;
11221122 };
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];
11241124 Some(crate::PageDrag {
11251125 space,
11261126 top: drag.live().then(|| drag.corner(self.pointer)[1] - origin),
......@@ -1142,10 +1142,10 @@ impl State {
11421142 ) {
11431143 let mine = |what: &Dragged| matches!(what, Dragged::Page(_));
11441144 self.settle(mine, settled);
1145 let held =
1146 held.and_then(|space| Some((Dragged::Page(space), self.ui.rect(self.ui.id(space))?)));
1145 let held = held
1146 .and_then(|space| Some((Dragged::Page(space), self.ui.laid_out(self.ui.id(space))?)));
11471147 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()));
11491149 let dropped = self.follow_drag(held, mine);
11501150 let (
11511151 Some(session),
......@@ -1217,7 +1217,7 @@ impl State {
12171217 let Dragged::Tab(index) = drag.what else {
12181218 return None;
12191219 };
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];
12211221 Some(ui::shell::Dragged {
12221222 index,
12231223 start: drag.live().then_some(start),
......@@ -1239,7 +1239,7 @@ impl State {
12391239 let held = held.and_then(|tab| {
12401240 Some((
12411241 Dragged::Tab(tab),
1242 self.ui.rect(ui::shell::tab_id(row, tab))?,
1242 self.ui.laid_out(ui::shell::tab_id(row, tab))?,
12431243 ))
12441244 });
12451245 let dropped = self.follow_drag(held, mine);
crates/snowbound/src/options.rs+6-6
......@@ -521,8 +521,8 @@ impl State {
521521 // The section in the top quarter of the list, or the last once the list is scrolled to
522522 // its end.
523523 let current = options.picked.or_else(|| {
524 let [_, top, _, bottom] = ui.rect(list())?;
525 let starts = |id| ui.rect(id).map(|rect: [f32; 4]| rect[1]);
524 let [_, top, _, bottom] = ui.laid_out(list())?;
525 let starts = |id| ui.laid_out(id).map(|rect: [f32; 4]| rect[1]);
526526 let end = starts(list().child("top")).is_some_and(|start| start < top - 1.0)
527527 && starts(list().child("end")).is_some_and(|end| end <= bottom + 1.0);
528528 sections
......@@ -886,7 +886,7 @@ fn dropdown(ui: &mut Ui, name: &str, choices: &[&str], chosen: usize, width: f32
886886 ..Item::default()
887887 })
888888 .collect();
889 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
889 let anchor = Anchor::Below(combo);
890890 ui::popup::menu(ui, menu, anchor, &items, None)
891891}
892892
......@@ -968,7 +968,7 @@ fn renderer(state: &mut State, options: &mut Options, name: &str) {
968968 ..Item::default()
969969 })
970970 .collect();
971 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
971 let anchor = Anchor::Below(combo);
972972 if let Some(index) = ui::popup::menu(ui, menu, anchor, &items, None) {
973973 let picked = Backend::PLATFORM[index];
974974 options.renderer = if Some(picked) == automatic {
......@@ -1044,7 +1044,7 @@ fn font_face(state: &mut State, options: &mut Options, name: &str) {
10441044 ..Item::default()
10451045 })
10461046 .collect();
1047 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
1047 let anchor = Anchor::Over(combo);
10481048 if let Some(index) = ui::popup::menu(ui, self::fonts(), anchor, &items, Some(name)) {
10491049 font.face = fonts[index].clone();
10501050 }
......@@ -1083,7 +1083,7 @@ fn font_color(state: &mut State, options: &mut Options, name: &str) {
10831083 .iter()
10841084 .map(|&(color, name)| (canvas::gpu::colorref(color), name))
10851085 .collect();
1086 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
1086 let anchor = Anchor::Below(combo);
10871087 if let Some(chosen) = ui::popup::colors(ui, font_colors(), anchor, "Automatic", &swatches, 10) {
10881088 font.color = chosen.and_then(|chosen| {
10891089 let at = swatches.iter().position(|(swatch, _)| *swatch == chosen)?;
crates/snowbound/src/palette.rs+2-2
......@@ -224,7 +224,7 @@ impl State {
224224 ui::popup::submenus(&mut self.ui, id(), &items[0], |index| {
225225 commands[index].submenu.then(|| submenu(index))
226226 });
227 let anchor = Anchor::Right(self.ui.rect(id()).unwrap_or_default());
227 let anchor = Anchor::Right(id());
228228 for (index, row) in commands.iter().enumerate() {
229229 if let (Some(target), Some(action)) = (&row.target, &row.action)
230230 && let Some(chosen) = self.submenu(submenu(index), action, target, anchor)
......@@ -307,7 +307,7 @@ impl State {
307307 (action, Item { separated, ..item })
308308 },
309309 ));
310 let anchor = Anchor::Right(self.ui.rect(id()).unwrap_or_default());
310 let anchor = Anchor::Right(id());
311311 if let Some(action) =
312312 self.action_menu(panel, anchor, Some("Search actions"), &target, &actions)
313313 {
crates/snowbound/src/pane.rs+1-1
......@@ -360,7 +360,7 @@ fn choice(
360360 ..Default::default()
361361 })
362362 .collect();
363 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
363 let anchor = Anchor::Below(combo);
364364 ui::popup::menu(ui, popup(name), anchor, &items, None)
365365}
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
9999 ..Item::default()
100100 })
101101 .collect();
102 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
102 let anchor = Anchor::Below(combo);
103103 ui::popup::menu(ui, menu, anchor, &items, None)
104104}
105105
crates/snowbound/src/properties.rs+1-1
......@@ -175,7 +175,7 @@ impl State {
175175 ..Item::default()
176176 })
177177 .collect();
178 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
178 let anchor = Anchor::Below(combo);
179179 if let Some(index) = ui::popup::menu(ui, colors(), anchor, &items, None) {
180180 dialog.color = Some(SECTION_COLORS[index].0);
181181 }
crates/snowbound/src/protection.rs+36-9
......@@ -5,7 +5,7 @@
55use crate::{Command, Library, State, art, manage::Structure};
66use accesskit::Role;
77use 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};
99use winit::keyboard::NamedKey;
1010use zeroize::Zeroizing;
1111
......@@ -133,6 +133,7 @@ impl State {
133133 shown,
134134 None,
135135 None,
136 None,
136137 section,
137138 crate::TAB_ROW,
138139 strip,
......@@ -165,24 +166,48 @@ impl State {
165166 let ui = &mut self.ui;
166167 let entered =
167168 ui::popup::navigation(ui, &[notice()], &[NamedKey::Enter]).contains(&NamedKey::Enter);
168 let area = ui.open(
169 ui.open(
169170 "locked",
170171 Spec {
172 axis: Axis::Y,
171173 size: [fill(), fill()],
172174 fill: Some(ui::mix(theme.paper, theme.chip, 0.6)),
173175 ..Spec::default()
174176 },
175177 );
176 let [left, top, right, bottom] = ui.rect(area).unwrap_or_default();
177 let width = 380.0_f32.min(right - left);
178 // A fifth of the way down, centred across, narrowing with a narrow page.
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)]);
178201 let open = ui.open_as(
179202 notice(),
180203 Spec {
181 flags: Flags::FLOAT | Flags::CLICKABLE | Flags::FOCUSABLE,
182 size: [px(width), children()],
183 position: [
184 ((right - left - width) / 2.0).max(0.0),
185 ((bottom - top) * 0.2).max(0.0),
204 flags: Flags::CLICKABLE | Flags::FOCUSABLE,
205 size: [
206 Extent {
207 size: ui::Size::Pixels(380.0),
208 strictness: 0.0,
209 },
210 children(),
186211 ],
187212 gap: 12.0,
188213 role: Some(Role::Button),
......@@ -230,6 +255,8 @@ impl State {
230255 );
231256 ui.close();
232257 ui.close();
258 room(ui, "after", [fill(), px(0.0)]);
259 ui.close();
233260 ui.close();
234261 ui.theme = chrome;
235262 if ui.signal(open).clicked || entered {
crates/snowbound/src/recording.rs+22-8
......@@ -7,7 +7,7 @@ use crate::{State, media, platform};
77use canvas::recording::{RATE, video};
88use onestore::page::Attachment;
99use 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};
1111use winit::keyboard::NamedKey;
1212
1313/// What records or plays now.
......@@ -273,11 +273,7 @@ impl State {
273273
274274 /// The transport over the page's foot while something records or plays: OneNote's
275275 /// Recording and Playback tabs, with a video's pictures above.
276 pub(crate) fn transport(
277 &mut self,
278 theme: &Theme,
279 page: [f32; 4],
280 ) -> Result<(), Box<dyn Error>> {
276 pub(crate) fn transport(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> {
281277 let row = theme.font_size * 2.0;
282278 if matches!(self.media, Media::Saving { .. }) {
283279 self.stop_recording(false)?;
......@@ -354,13 +350,30 @@ impl State {
354350 .as_ref()
355351 .map_or(0.0, |image| image.size()[1] as f32 + 6.0);
356352 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 );
357371 self.ui.open(
358372 "transport",
359373 Spec {
360 flags: Flags::FLOAT | Flags::CLICKABLE,
374 flags: Flags::CLICKABLE,
361375 axis: Axis::Y,
362376 size: [children(), px(height)],
363 position: [16.0, page[3] - page[1] - height - 16.0],
364377 fill: Some(theme.popup),
365378 shadow: Some(theme.shadow),
366379 radius: 8.0,
......@@ -435,6 +448,7 @@ impl State {
435448 }
436449 self.ui.close();
437450 self.ui.close();
451 self.ui.close();
438452 if let Some(at) = seek
439453 && let Media::Playing(playback) = &mut self.media
440454 {
crates/snowbound/src/rename.rs+17-43
......@@ -54,6 +54,23 @@ pub fn field() -> Id {
5454 Id::ROOT.child("rename")
5555}
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
5774/// The rename field, `height` tall, editing `name`: whether Enter kept or Escape dropped
5875/// the name typed.
5976pub fn edit(ui: &mut Ui, theme: &Theme, name: &mut String, height: f32) -> Option<bool> {
......@@ -146,49 +163,6 @@ impl State {
146163 }
147164 }
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
192166 /// Gives page `space` of the open section the title `name`.
193167 fn retitle(&mut self, space: ExGuid, name: String) -> Result<(), Box<dyn Error>> {
194168 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
265265 ..Item::default()
266266 })
267267 .collect();
268 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
268 let anchor = Anchor::Below(combo);
269269 let chosen = ui::popup::menu(ui, menu, anchor, &items, None);
270270 ui.close();
271271 chosen
crates/snowbound/src/search.rs+9-14
......@@ -780,8 +780,7 @@ impl State {
780780 self.ui.close();
781781 // Results opened this frame build from the next, as the box's field was built.
782782 if searching || !self.ui.popup_open(results()) {
783 let rect = self.ui.rect(box_id).unwrap_or_default();
784 self.results_popup(theme, rect)?;
783 self.results_popup(theme, box_id)?;
785784 }
786785 self.scope_popup();
787786 Ok(())
......@@ -791,8 +790,7 @@ impl State {
791790 let button = self.ui.id("scope");
792791 if ui::shell::tool_button(&mut self.ui, "scope", art::SEARCH, theme.text_dim, None).pressed
793792 {
794 let rect = self.ui.rect(button).unwrap_or_default();
795 self.search.anchor = Some(Anchor::Below(rect));
793 self.search.anchor = Some(Anchor::Below(button));
796794 self.ui.open_popup(scope_menu());
797795 }
798796 let open = self.ui.popup_open(scope_menu());
......@@ -856,7 +854,7 @@ impl State {
856854 }
857855
858856 /// 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>> {
860858 if !self.ui.popup_open(results()) {
861859 // Closed by Esc or a press elsewhere: the search ends.
862860 if std::mem::take(&mut self.search.open) {
......@@ -879,21 +877,18 @@ impl State {
879877 if self.ui.focused() == Some(results()) {
880878 self.ui.set_focus(Some(field()));
881879 }
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);
885880 self.ui.open_as(
886881 results(),
887882 Spec {
888883 axis: Axis::Y,
889 size: [px(width), children()],
884 size: [px(RESULTS), children()],
890885 fill: Some(theme.popup),
891886 border: Some(theme.chip),
892887 shadow: Some(theme.shadow),
893888 radius: 6.0,
894889 pad: [4.0, 4.0],
895890 gap: 4.0,
896 anchor: Some(anchor),
891 anchor: Some(Anchor::Over(over)),
897892 role: Some(Role::Dialog),
898893 ..Spec::default()
899894 },
......@@ -985,8 +980,7 @@ impl State {
985980 )
986981 .clicked
987982 {
988 let rect = self.ui.rect(change).unwrap_or_default();
989 self.search.anchor = Some(Anchor::Below(rect));
983 self.search.anchor = Some(Anchor::Below(change));
990984 self.ui.open_popup(scope_menu());
991985 }
992986 self.ui.leaf(
......@@ -1016,8 +1010,9 @@ impl State {
10161010 count: found.len(),
10171011 };
10181012 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]);
1020 let view = content.min((window - bottom - 80.0).max(RESULT));
1013 // Down to the window's foot from the box, as last laid out.
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));
10211016 let before = self.search.selected;
10221017 let places: Vec<String> = found
10231018 .iter()
crates/snowbound/src/sidebar.rs+62-24
......@@ -298,7 +298,7 @@ fn sidebar(
298298 }
299299 if row.dragging
300300 && !renamed
301 && let Some(rect) = ui.rect(id)
301 && let Some(rect) = ui.laid_out(id)
302302 {
303303 tree.held_notebook = Some(Entry {
304304 notebook: index,
......@@ -316,7 +316,7 @@ fn sidebar(
316316 ui.close();
317317 }
318318 ui.close();
319 tree.blocks.extend(ui.rect(block));
319 tree.blocks.extend(ui.laid_out(block));
320320 }
321321 if let Some((_, slot)) = tree.lifted_notebook {
322322 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
632632 let id = ui.id(part);
633633 let height = ui.animate(id, if here { ROW } else { 0.0 });
634634 if here {
635 tree.gap = ui.rect(id);
635 tree.gap = ui.laid_out(id);
636636 }
637637 if height > 0.0 {
638638 ui.leaf(
......@@ -657,7 +657,7 @@ fn unfolding(ui: &mut Ui, part: impl std::hash::Hash, unfolded: bool) -> bool {
657657 ui::children()
658658 } else {
659659 let rows = ui
660 .rect(outer.child("rows"))
660 .laid_out(outer.child("rows"))
661661 .map_or(0.0, |rect| rect[3] - rect[1]);
662662 let height = ui.animate(outer, if unfolded { rows } else { 0.0 });
663663 if !unfolded && height == 0.0 {
......@@ -687,7 +687,7 @@ fn unfolding(ui: &mut Ui, part: impl std::hash::Hash, unfolded: bool) -> bool {
687687
688688/// Records a section or group row, built as `id`, for dragging.
689689fn 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 {
691691 return;
692692 };
693693 let entry = Entry {
......@@ -929,7 +929,7 @@ impl crate::State {
929929 gap,
930930 ..
931931 } = tree;
932 let corner = self.ui.rect(rows_id).unwrap_or_default();
932 let corner = self.ui.laid_out(rows_id).unwrap_or_default();
933933 let found = ghost.is_some();
934934 let (ghost_held, settled) = self.lifted_row(theme, ghost, gap, &rows, corner);
935935 let settled = settled || !found;
......@@ -987,17 +987,9 @@ impl crate::State {
987987 if self.temporary {
988988 return;
989989 }
990 let (nav, toggle) = if self.navigation_bar_right {
991 // The notebook button stands at the body's far edge, as laid out last frame.
992 let edge = self
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 };
990 let far = self.navigation_bar_right;
991 let toggle = (MARGIN + ROW_PAD - (RAIL - ICON) / 2.0) * width / WIDTH;
992 let nav = if far { 0.0 } else { toggle + RAIL };
1001993 let pad = [
1002994 (RAIL - ui::shell::TOOL) / 2.0,
1003995 (RAIL - ui::shell::TOOL) / 2.0 + DROP,
......@@ -1021,10 +1013,32 @@ impl crate::State {
10211013 }
10221014 }
10231015 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 }
10241034 self.ui.open(
10251035 "toggle",
10261036 Spec {
1027 flags: Flags::FLOAT | Flags::CLIP,
1037 flags: if far {
1038 Flags::CLIP
1039 } else {
1040 Flags::FLOAT | Flags::CLIP
1041 },
10281042 size: [px(RAIL), px(height)],
10291043 position: [toggle, 0.0],
10301044 pad,
......@@ -1043,6 +1057,9 @@ impl crate::State {
10431057 }
10441058 crate::tip(&mut self.ui, Cmd::Sidebar);
10451059 self.ui.close();
1060 if far {
1061 self.ui.close();
1062 }
10461063 }
10471064
10481065 /// The dragged row, `ghost`, drawn over the others in the rows' box `corner`: following
......@@ -1499,25 +1516,43 @@ impl crate::State {
14991516 const BUTTON: [f32; 2] = [240.0, 32.0];
15001517 let theme = self.page_area_theme();
15011518 let chrome = std::mem::replace(&mut self.ui.theme, theme.clone());
1502 let [left, top, right, bottom] = self.ui.rect(id).unwrap_or_default();
15031519 self.ui.open_as(
15041520 id,
15051521 Spec {
1522 axis: Axis::Y,
15061523 size: [fill(), fill()],
15071524 fill: Some(theme.paper),
15081525 ..Spec::default()
15091526 },
15101527 );
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)]);
15111551 self.ui.open(
15121552 "content",
15131553 Spec {
1514 flags: Flags::FLOAT,
15151554 axis: Axis::Y,
15161555 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 ],
15211556 gap: 10.0,
15221557 ..Spec::default()
15231558 },
......@@ -1558,6 +1593,9 @@ impl crate::State {
15581593 self.commands.extend(chosen);
15591594 let saved = crate::server::saved_servers(&mut self.ui, servers);
15601595 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)]);
15611599 self.ui.close();
15621600 self.ui.theme = chrome;
15631601 if let Some(saved) = saved {
crates/snowbound/src/symbol.rs+1-1
......@@ -547,7 +547,7 @@ impl State {
547547 ui.close();
548548 ui.close();
549549
550 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
550 let anchor = Anchor::Below(combo);
551551 if ui.popup_open(subsets()) {
552552 // Each subset that shows a character, and its first.
553553 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) {
290290 // Unfolds to the titles' height, scrolling past a few dozen lines.
291291 let list = ui.id("changes");
292292 let rows = ui
293 .rect(list.child("rows"))
293 .laid_out(list.child("rows"))
294294 .map_or(0.0, |rect| rect[3] - rect[1]);
295295 let height = ui.animate(list, if listed { rows.min(line * 9.0) } else { 0.0 });
296296 if listed || height > 0.0 {
......@@ -533,7 +533,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
533533 let state = sync.state();
534534 let syncing = !facts.offline && state == SyncState::Syncing;
535535 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;
537537 let showing = !facts.offline && shows_sync(ui, button(), syncing, shown);
538538 let (mut headline, mut icon) = describe(&sync, facts.offline, !showing);
539539 let host = facts.place.first().map_or("the notebook", String::as_str);
......@@ -731,7 +731,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
731731 ..Spec::default()
732732 },
733733 );
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]);
735735 let filled = width * fraction;
736736 ui.mark([0.0, 0.0, filled, 6.0], theme.accent, 3.0);
737737 // A sheen sweeps the filled part while the sync runs, so a long section still moves.
......@@ -1075,10 +1075,7 @@ impl State {
10751075 session.sync = session.section.sync_status()?;
10761076 let (library, anchor) = match &self.sync_notebook {
10771077 Some((library, at)) => (Arc::clone(library), Anchor::Point(*at)),
1078 None => (
1079 Arc::clone(&session.library),
1080 Anchor::Below(ui.rect(button()).unwrap_or_default()),
1081 ),
1078 None => (Arc::clone(&session.library), Anchor::Below(button())),
10821079 };
10831080 let own = Arc::ptr_eq(&library, &session.library);
10841081 let offline = library::offline();
......@@ -1244,7 +1241,7 @@ mod tests {
12441241 if !ui.popup_open(id()) {
12451242 ui.open_popup(id());
12461243 }
1247 open(ui, Anchor::Below(ui.rect(button()).unwrap_or_default()));
1244 open(ui, Anchor::Below(button()));
12481245 build(ui, facts);
12491246 ui.close();
12501247 ui.end();
......@@ -1352,7 +1349,7 @@ mod tests {
13521349 .unwrap();
13531350 let mapped = buffer.get_mapped_range(..).unwrap();
13541351 let bottom = bottom
1355 .unwrap_or(ui.rect(id()).unwrap()[3] + 24.0)
1352 .unwrap_or(ui.laid_out(id()).unwrap()[3] + 24.0)
13561353 .min(SIZE[1]);
13571354 let height = (bottom * SCALE) as u32;
13581355 let pixels: Vec<u8> = mapped
......@@ -1382,7 +1379,7 @@ mod tests {
13821379 }
13831380
13841381 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()
13861383 }
13871384
13881385 #[test]
......@@ -1547,7 +1544,7 @@ mod tests {
15471544 let mut ui = ui(Appearance::Light);
15481545 let mut now = Instant::now();
15491546 settle(&mut ui, &mut now, &made());
1550 let [_, top, _, bottom] = ui.rect(id()).unwrap();
1547 let [_, top, _, bottom] = ui.laid_out(id()).unwrap();
15511548 (name, bottom - top)
15521549 })
15531550 .collect();
......@@ -1570,7 +1567,7 @@ mod tests {
15701567 let mut now = Instant::now();
15711568 let facts = Facts { offline, ..made() };
15721569 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()));
15741571 }
15751572 assert!(
15761573 places.windows(2).all(|pair| pair[0] == pair[1]),
......@@ -1594,7 +1591,7 @@ mod tests {
15941591 let mut now = Instant::now();
15951592 let facts = facts();
15961593 settle(&mut ui, &mut now, &facts);
1597 assert!(ui.rect(id()).is_some(), "{name}");
1594 assert!(ui.laid_out(id()).is_some(), "{name}");
15981595 if let (Some(output), Some(gpu)) = (&output, &mut gpu) {
15991596 gpu.save(&ui, &output.join(format!("{name}-{theme}.png")), None);
16001597 }
crates/snowbound/src/tags.rs+1-1
......@@ -341,7 +341,7 @@ fn picker(
341341 if ui.signal(button).pressed {
342342 ui.open_popup(menu);
343343 }
344 Anchor::Below(ui.rect(button).unwrap_or_default())
344 Anchor::Below(button)
345345}
346346
347347/// The frame of a dialog titled `title`, centred in the window.
crates/snowbound/src/templates.rs+8-6
......@@ -112,6 +112,10 @@ fn strip(
112112 shown.push(pick);
113113 }
114114 shown.push(more);
115 let tiles = (shown.iter())
116 .map(|(_, label)| GAP + tile_width(ui, label))
117 .sum::<f32>()
118 - GAP;
115119 let mut chosen = None;
116120 ui.open(
117121 "templates",
......@@ -177,15 +181,12 @@ fn strip(
177181 }
178182 ui.close();
179183 }
180 let width = ui
181 .rect(ui.id("tiles"))
182 .map_or(0.0, |rect| rect[2] - rect[0]);
183184 let close = ui.leaf(
184185 "dismiss",
185186 Spec {
186187 flags: Flags::FLOAT | Flags::CLICKABLE,
187188 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],
189190 icon: Some(art::CLOSE),
190191 color: Some(theme.text_dim),
191192 fill: Some(theme.popup),
......@@ -437,7 +438,8 @@ impl crate::State {
437438 self.templates = View::Strip;
438439 return;
439440 }
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 {
441443 return;
442444 };
443445 let viewport = self.view.viewport;
......@@ -503,7 +505,7 @@ impl crate::State {
503505 /// The Page Color menu's Show All: every template's art, one of which goes behind the
504506 /// open page.
505507 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 {
507509 return;
508510 };
509511 let paper = Paper {
crates/snowbound/src/themes.rs+6-6
......@@ -446,7 +446,7 @@ impl State {
446446 ..Item::default()
447447 })
448448 .collect();
449 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
449 let anchor = Anchor::Over(combo);
450450 if let Some(index) = ui::popup::menu(ui, popup("styles"), anchor, &items, None) {
451451 dialog.style = index;
452452 }
......@@ -475,7 +475,7 @@ impl State {
475475 ..Item::default()
476476 })
477477 .collect();
478 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
478 let anchor = Anchor::Over(combo);
479479 if let Some(index) = ui::popup::menu(ui, popup("fonts"), anchor, &items, Some("Font")) {
480480 let font = names[index].to_owned();
481481 change = Some(Change::Font(font));
......@@ -495,7 +495,7 @@ impl State {
495495 ..Item::default()
496496 })
497497 .collect();
498 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
498 let anchor = Anchor::Over(combo);
499499 if let Some(index) = ui::popup::menu(ui, popup("sizes"), anchor, &items, None) {
500500 let size = crate::SIZES[index];
501501 change = Some(Change::Size(size));
......@@ -521,7 +521,7 @@ impl State {
521521 .iter()
522522 .map(|&(color, name)| (canvas::gpu::colorref(color), name))
523523 .collect();
524 let anchor = Anchor::Below(ui.rect(button).unwrap_or_default());
524 let anchor = Anchor::Below(button);
525525 let buttons = ["Automatic", "Theme"];
526526 if let Some(picked) =
527527 ui::popup::color_grid(ui, popup("colors"), anchor, &buttons, &swatches, 10)
......@@ -556,7 +556,7 @@ impl State {
556556 ..Item::default()
557557 })
558558 .collect();
559 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
559 let anchor = Anchor::Over(combo);
560560 if let Some(index) = ui::popup::menu(ui, popup(part), anchor, &items, None) {
561561 let points = SPACING[index];
562562 change = Some(Change::Spacing(part == "before", points));
......@@ -634,7 +634,7 @@ impl State {
634634 ..Item::default()
635635 })
636636 .collect();
637 let anchor = Anchor::Below(ui.rect(ui.id(part)).unwrap_or_default());
637 let anchor = Anchor::Below(ui.id(part));
638638 ui::popup::menu(ui, menu, anchor, &items, None)
639639 };
640640 let remove = match targets[..] {
crates/snowbound/src/unpack.rs+1-1
......@@ -138,7 +138,7 @@ impl State {
138138 ..Item::default()
139139 })
140140 .collect();
141 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
141 let anchor = Anchor::Below(combo);
142142 if let Some(index) = ui::popup::menu(ui, colors, anchor, &items, None) {
143143 dialog.color = Some(SECTION_COLORS[index].0);
144144 }
crates/ui/examples/popups.rs+17-35
......@@ -75,8 +75,7 @@ fn main() {
7575 ui::popup::menu(ui, context, Anchor::Point([140.0, 70.0]), &items[..8], None);
7676 }),
7777 ("combo", fonts, &|ui, [combo, _]| {
78 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
79 ui::popup::menu(ui, fonts, anchor, &items, Some("Calibri"));
78 ui::popup::menu(ui, fonts, Anchor::Over(combo), &items, Some("Calibri"));
8079 }),
8180 ("search", search, &|ui, [combo, _]| {
8281 results(ui, search, combo)
......@@ -138,33 +137,27 @@ fn results(ui: &mut Ui, id: Id, combo: Id) {
138137 return;
139138 }
140139 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);
145140 ui.open_as(
146141 id,
147142 Spec {
148143 axis: ui::Axis::Y,
149 size: [px(width), ui::children()],
144 size: [px(300.0), ui::children()],
150145 fill: Some(theme.popup),
151146 border: Some(theme.chip),
152147 shadow: Some(theme.shadow),
153148 radius: 6.0,
154149 pad: [4.0, 4.0],
155150 gap: 4.0,
156 anchor: Some(anchor),
151 anchor: Some(Anchor::Over(combo)),
157152 ..Spec::default()
158153 },
159154 );
155 // Stands in for the combo, as tall, widening with the panel.
160156 ui.leaf(
161157 "box",
162158 Spec {
163159 flags: ui::Flags::STILL,
164 size: [
165 px(right - left + (width - 8.0 - (right - left)) * open),
166 px(bottom - top),
167 ],
160 size: [fill(), px(0.0)],
168161 text: Some("Search All Notebooks (⌘E)"),
169162 color: Some(theme.text_dim),
170163 fill: Some(theme.base),
......@@ -253,8 +246,7 @@ fn keystroke(
253246 let items = font_items();
254247 let id = Id::ROOT.child("fonts");
255248 let build = |ui: &mut Ui, [combo, _]: [Id; 2]| {
256 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
257 ui::popup::menu(ui, id, anchor, &items, Some("Calibri"));
249 ui::popup::menu(ui, id, Anchor::Over(combo), &items, Some("Calibri"));
258250 };
259251 scene.open(id, Vec::new(), build);
260252 let mut panels = vec![paint(device, queue, renderer, &scene.ui)];
......@@ -543,10 +535,6 @@ impl Scene {
543535 }
544536}
545537
546fn below(ui: &Ui, id: Id) -> Anchor {
547 Anchor::Below(ui.rect(id).unwrap_or_default())
548}
549
550538fn key(named: NamedKey) -> Event {
551539 Event::Key {
552540 key: Key::Named(named),
......@@ -656,8 +644,7 @@ fn fonts(scene: &mut Scene) {
656644 let items = font_items();
657645 let id = Id::ROOT.child("fonts");
658646 scene.open(id, typed("ca"), |ui, [combo, _]| {
659 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
660 ui::popup::menu(ui, id, anchor, &items, Some("Calibri"));
647 ui::popup::menu(ui, id, Anchor::Over(combo), &items, Some("Calibri"));
661648 });
662649}
663650
......@@ -707,16 +694,16 @@ fn colors(scene: &mut Scene) {
707694 let swatches = office_colors();
708695 let id = Id::ROOT.child("font color");
709696 scene.open(id, Vec::new(), |ui, [_, split]| {
710 let anchor = below(ui, split);
697 let anchor = Anchor::Below(split);
711698 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);
712699 });
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();
714701 scene
715702 .ui
716703 .event(Event::PointerMoved([cell[0] + 5.0, cell[1] + 5.0]));
717704 for _ in 0..30 {
718705 scene.frame(|ui, [_, split]| {
719 let anchor = below(ui, split);
706 let anchor = Anchor::Below(split);
720707 ui::popup::colors(ui, id, anchor, "Automatic", &swatches, 10);
721708 });
722709 }
......@@ -743,7 +730,7 @@ fn highlight(scene: &mut Scene) {
743730 .map(|(r, g, b)| (draw::srgb(r, g, b), ""));
744731 let id = Id::ROOT.child("highlight");
745732 scene.open(id, vec![key(NamedKey::ArrowDown)], |ui, [_, split]| {
746 let anchor = below(ui, split);
733 let anchor = Anchor::Below(split);
747734 ui::popup::colors(ui, id, anchor, "No Color", &swatches, 5);
748735 });
749736}
......@@ -820,21 +807,16 @@ fn tags(scene: &mut Scene) {
820807 ..Item::default()
821808 });
822809 let id = Id::ROOT.child("tags");
823 scene.open(id, Vec::new(), |ui, [combo, _]| {
824 let rect = ui.rect(combo).unwrap_or_default();
825 let anchor = Anchor::Below([rect[0] + 200.0, rect[1], rect[0] + 222.0, rect[3]]);
826 ui::popup::menu(ui, id, anchor, &tags, Some("Filter tags"));
827 });
828 let row = scene.ui.rect(id.child("rows").child(3_usize)).unwrap();
810 let build = |ui: &mut Ui, [_, split]: [Id; 2]| {
811 ui::popup::menu(ui, id, Anchor::Below(split), &tags, Some("Filter tags"));
812 };
813 scene.open(id, Vec::new(), build);
814 let row = scene.ui.laid_out(id.child("rows").child(3_usize)).unwrap();
829815 scene
830816 .ui
831817 .event(Event::PointerMoved([row[0] + 40.0, row[1] + 8.0]));
832818 for _ in 0..3 {
833 scene.frame(|ui, [combo, _]| {
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 });
819 scene.frame(build);
838820 }
839821}
840822
crates/ui/src/access/tests.rs+6-7
......@@ -187,14 +187,13 @@ fn toolbar(ui: &mut Ui, bold: bool) -> Vec<&'static str> {
187187 ..popup::Item::default()
188188 },
189189 ];
190 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
191 popup::menu(ui, font_menu(), anchor, &items, Some("Font"));
190 popup::menu(ui, font_menu(), Anchor::Over(combo), &items, Some("Font"));
192191 let paste = [popup::Item {
193192 text: "Keep Text Only",
194193 shortcut: "⌘⇧V",
195194 ..popup::Item::default()
196195 }];
197 let anchor = Anchor::Below(ui.rect(ui.id("paste")).unwrap_or_default());
196 let anchor = Anchor::Below(ui.id("paste"));
198197 popup::menu(ui, more_menu(), anchor, &paste, None);
199198 clicked
200199}
......@@ -256,7 +255,7 @@ fn a_disabled_combo_shows_its_value_but_takes_no_clicks_or_focus() {
256255 });
257256 };
258257 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();
260259 let middle = [(left + right) / 2.0, (top + bottom) / 2.0];
261260 ui.event(Event::PointerMoved(middle));
262261 for pressed in [true, false] {
......@@ -317,7 +316,7 @@ fn a_row_opening_a_menu_says_so_instead_of_naming_keys() {
317316 ui.open_popup(menu);
318317 for _ in 0..2 {
319318 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);
321320 });
322321 }
323322 let tree = tree(&mut ui);
......@@ -348,7 +347,7 @@ fn swatches_are_named_by_their_colour_names_or_hex() {
348347 ui.open_popup(grid);
349348 for _ in 0..2 {
350349 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);
352351 });
353352 }
354353 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
782781 }
783782 assert_eq!((ui.focused(), chosen), (segment(0), 0), "an arrow picks");
784783 // 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();
786785 ui.event(Event::PointerMoved([
787786 (left + right) / 2.0,
788787 (top + bottom) / 2.0,
crates/ui/src/layout.rs+128-36
......@@ -1,5 +1,6 @@
11use 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,
34};
45use std::collections::HashMap;
56
......@@ -7,8 +8,9 @@ use std::collections::HashMap;
78/// ancestors (pre-order), sizes summed from children (post-order), overflow taken back
89/// (by folding a row's groups for what space sized by ancestors can't give, then from that
910/// 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, so
11/// index order is pre-order.
11/// solved width wrap or shorten before heights are solved. Boxes are in build order, so a
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.
1214pub(crate) fn solve(
1315 nodes: &mut [Built],
1416 states: &HashMap<Id, State>,
......@@ -22,15 +24,97 @@ pub(crate) fn solve(
2224 nodes[folded].hidden = true;
2325 }
2426 }
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) {
25108 for axis in 0..2 {
26109 if axis == 1 {
27 fit_labels(nodes, texts, frame);
110 fit_labels(nodes, members, texts, frame);
28111 }
29 for node in nodes.iter_mut() {
112 for &index in members {
113 let node = &mut nodes[index];
30114 node.computed[axis] = match node.size[axis].size {
31115 Size::Pixels(pixels) => match node.anchor {
32 Some(Anchor::Over(rect)) if axis == 0 => {
33 let from = rect[2] - rect[0];
116 Some(Anchor::Over(_)) if axis == 0 => {
117 let from = node.around[2] - node.around[0];
34118 from + (pixels - from) * node.open
35119 }
36120 _ => pixels,
......@@ -51,8 +135,8 @@ pub(crate) fn solve(
51135 Size::Fraction(_) | Size::Children => 0.0,
52136 };
53137 }
54 fit_popups(nodes, axis);
55 for index in 1..nodes.len() {
138 fit_popups(nodes, members, axis);
139 for &index in members.iter().filter(|index| **index != 0) {
56140 if let Size::Fraction(fraction) = nodes[index].size[axis].size {
57141 if stretches(nodes, index, axis) {
58142 nodes[index].computed[axis] = 0.0;
......@@ -66,15 +150,15 @@ pub(crate) fn solve(
66150 nodes[index].computed[axis] = room.max(0.0) * fraction;
67151 }
68152 }
69 for index in (0..nodes.len()).rev() {
153 for &index in members.iter().rev() {
70154 if nodes[index].size[axis].size == Size::Children {
71155 let content = flow(nodes, index, axis);
72156 nodes[index].computed[axis] = content + 2.0 * nodes[index].pad[axis];
73157 }
74158 }
75 yield_popups(nodes, axis);
76 fit_popups(nodes, axis);
77 for index in 0..nodes.len() {
159 yield_popups(nodes, members, axis);
160 fit_popups(nodes, members, axis);
161 for &index in members {
78162 let node = &nodes[index];
79163 if node.children.is_empty() || (axis == 1 && node.flags.contains(Flags::SCROLL)) {
80164 continue;
......@@ -144,24 +228,30 @@ pub(crate) fn solve(
144228 }
145229 }
146230 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 }
148241 let mut cursor = nodes[index].pad[axis];
149242 // Where a popup widening over its anchor lays its children out, from where it is.
150243 let shift = match (nodes[index].anchor, nodes[index].size[axis].size) {
151244 (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]
153247 }
154248 _ => 0.0,
155249 };
156250 for child in nodes[index].children.clone() {
157 nodes[child].relative[axis] = if let Some(anchor) = nodes[child].anchor {
158 let shown = nodes[child].computed[axis];
159 let size = match nodes[child].size[axis].size {
160 Size::Pixels(pixels) => pixels,
161 _ => shown,
162 };
163 anchor.place(axis, size, shown, window)
164 } else if nodes[child].flags.contains(Flags::FLOAT) {
251 if nodes[child].anchor.is_some() {
252 continue;
253 }
254 nodes[child].relative[axis] = if nodes[child].flags.contains(Flags::FLOAT) {
165255 nodes[child].position[axis]
166256 } else if nodes[child].hidden {
167257 nodes[index].pad[axis]
......@@ -182,13 +272,12 @@ pub(crate) fn solve(
182272 }
183273 }
184274 let snap = |value: f32| (value * scale).round() / scale;
185 nodes[0].rect = [
186 0.0,
187 0.0,
188 snap(nodes[0].computed[0]),
189 snap(nodes[0].computed[1]),
190 ];
191 for index in 1..nodes.len() {
275 for &index in members {
276 if index == 0 {
277 let [width, height] = nodes[0].computed;
278 nodes[0].rect = [0.0, 0.0, snap(width), snap(height)];
279 continue;
280 }
192281 let parent = nodes[index].parent;
193282 if nodes[index].hidden || nodes[parent].hidden {
194283 nodes[index].hidden = true;
......@@ -229,9 +318,10 @@ fn stretches(nodes: &[Built], index: usize, axis: usize) -> bool {
229318
230319/// Lets each popup sized loosely give way to the window, as far as its strictness lets it:
231320/// 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) {
233322 let window = nodes[0].computed[axis];
234 for node in &mut nodes[1..] {
323 for &index in members {
324 let node = &mut nodes[index];
235325 let room = match node.anchor {
236326 None => continue,
237327 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) {
246336
247337/// Shrinks each popup longer than the window lets it be on `axis`; one cut short vertically
248338/// scrolls what it holds.
249fn fit_popups(nodes: &mut [Built], axis: usize) {
339fn fit_popups(nodes: &mut [Built], members: &[usize], axis: usize) {
250340 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];
252343 if node.anchor.is_some() && node.computed[axis] > most {
253344 node.computed[axis] = most;
254345 if axis == 1 {
......@@ -374,8 +465,9 @@ fn least(nodes: &[Built], index: usize) -> f32 {
374465 own * node.size[0].strictness.clamp(0.0, 1.0)
375466}
376467
377fn fit_labels(nodes: &mut [Built], texts: &mut Texts, frame: u64) {
378 for node in nodes.iter_mut() {
468fn fit_labels(nodes: &mut [Built], members: &[usize], texts: &mut Texts, frame: u64) {
469 for &index in members {
470 let node = &mut nodes[index];
379471 let Some(label) = node.label.clone() else {
380472 continue;
381473 };
crates/ui/src/lib.rs+125-91
......@@ -167,7 +167,7 @@ impl Flags {
167167 /// The host paints it and receives the events routed to it.
168168 pub const CUSTOM: Flags = Flags(32);
169169 /// 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.
171171 pub const STILL: Flags = Flags(64);
172172
173173 pub(crate) fn contains(self, other: Flags) -> bool {
......@@ -219,14 +219,17 @@ fn fitting(room: f32) -> f32 {
219219}
220220
221221/// 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.
222223#[derive(Clone, Copy, Debug, PartialEq)]
223224pub enum Anchor {
224 /// Under the rectangle, from its leading edge, as a drop-down opens.
225 Below([f32; 4]),
226 /// Past the rectangle's trailing edge, from its top, as a submenu opens.
227 Right([f32; 4]),
228 /// Over the rectangle from its corner, as a combo box opens into its own list.
229 Over([f32; 4]),
225 /// Under the box, from its leading edge, as a drop-down opens; at least as wide as it.
226 Below(Id),
227 /// Past the trailing edge of the box, or of the popup holding it, level with the box's
228 /// top, as a submenu opens.
229 Right(Id),
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),
230233 /// At a point, as a context menu opens.
231234 Point([f32; 2]),
232235 /// Centred across the window near its top, over the interface, which dims, as a dialog
......@@ -238,24 +241,23 @@ pub enum Anchor {
238241}
239242
240243impl Anchor {
241 /// Where a popup `size` long on `axis` starts in a window `room` long: past the
242 /// anchor on the axis it opens along, level with it otherwise. Shown only `shown` long
243 /// 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 {
244 /// Where a popup `size` long on `axis` starts in a window `room` long: past `around`, the
245 /// rectangle it opens beside, on the axis it opens along, level with it otherwise. Shown
246 /// only `shown` long as it opens, it keeps the edge it would have at full size.
247 fn place(self, around: [f32; 4], axis: usize, size: f32, shown: f32, room: f32) -> f32 {
245248 let size = size.min(fitting(room));
246249 let shown = shown.min(size);
247250 let most = (room - POPUP_MARGIN - size).max(POPUP_MARGIN);
248 let (rect, along) = match self {
249 Anchor::Below(rect) => (rect, Some(1)),
250 Anchor::Right(rect) => (rect, Some(0)),
251 Anchor::Over(rect) => (rect, None),
252 Anchor::Point([x, y]) => ([x, y, x, y], Some(1)),
251 let along = match self {
252 Anchor::Below(_) | Anchor::Point(_) => Some(1),
253 Anchor::Right(_) => Some(0),
254 Anchor::Over(_) => None,
253255 Anchor::Dialog | Anchor::Top if axis == 0 => {
254256 return ((room - size) / 2.0).max(POPUP_MARGIN);
255257 }
256258 Anchor::Dialog | Anchor::Top => return (room / 8.0).clamp(POPUP_MARGIN, most),
257259 };
258 let [low, high] = [rect[axis], rect[axis + 2]];
260 let [low, high] = [around[axis], around[axis + 2]];
259261 let (first, second) = if along == Some(axis) {
260262 (high, low - size)
261263 } else {
......@@ -270,42 +272,6 @@ impl Anchor {
270272 }
271273 }
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
309275 /// Seconds the popup takes to open and to close.
310276 fn durations(self) -> [f32; 2] {
311277 if matches!(self, Anchor::Dialog | Anchor::Top) {
......@@ -534,6 +500,8 @@ struct Built {
534500 offset: [f32; 2],
535501 cursor: Option<CursorIcon>,
536502 anchor: Option<Anchor>,
503 /// The rectangle a popup opens beside, as layout resolves its anchor.
504 around: [f32; 4],
537505 /// How far open a popup over a box is, as it widens out of the box.
538506 open: f32,
539507 motion: Option<Motion>,
......@@ -604,12 +572,61 @@ struct Tip {
604572 due: Option<Instant>,
605573}
606574
607/// A popup's painting as it last showed, fading out since it closed.
608struct Closing {
575/// A popup painted: its anchor, the rectangle it opens beside, and its own.
576#[derive(Clone, Copy)]
577struct Placed {
609578 id: Id,
610 display: Vec<Display>,
611579 anchor: Anchor,
580 around: [f32; 4],
612581 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>,
613630 closed: Instant,
614631}
615632
......@@ -716,9 +733,8 @@ pub struct Ui {
716733 still: Vec<Display>,
717734 /// Where the popups' painting starts in `display`, for edges drawn beneath them.
718735 popups_painted: usize,
719 /// Each popup painted, its anchor and rectangle, and its painting's place after
720 /// `popups_painted`.
721 painted: Vec<(Id, Anchor, [f32; 4], std::ops::Range<usize>)>,
736 /// Each popup painted, and its painting's place after `popups_painted`.
737 painted: Vec<(Placed, std::ops::Range<usize>)>,
722738 closing: Vec<Closing>,
723739 tip: Option<Tip>,
724740 /// When a tooltip last showed.
......@@ -739,6 +755,8 @@ pub struct Ui {
739755 resume: Option<Id>,
740756 /// The nodes last sent to assistive technology.
741757 sent: HashMap<Id, accesskit::Node>,
758 /// Scrolling boxes to scroll to a box within each, once laid out.
759 scrolls: Vec<(Id, Id)>,
742760}
743761
744762impl Ui {
......@@ -786,6 +804,7 @@ impl Ui {
786804 focus_ring: false,
787805 resume: None,
788806 sent: HashMap::new(),
807 scrolls: Vec::new(),
789808 }
790809 }
791810
......@@ -867,21 +886,27 @@ impl Ui {
867886 Some(layout::narrowest(&self.nodes, index))
868887 }
869888
870 /// The box's rectangle from the latest layout; none for one not laid out yet.
871 pub fn rect(&self, id: Id) -> Option<[f32; 4]> {
889 /// Where box `id` lies in the latest layout: this frame's once `end` has run, otherwise
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]> {
872894 self.states.get(&id).and_then(|state| state.rect)
873895 }
874896
875 /// Scrolls `scroller`, a scrolling box, until `target` within it starts at its top, both
876 /// as last laid out, easing there.
897 /// The window's size in logical pixels this frame.
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.
877908 pub fn scroll_to(&mut self, scroller: Id, target: Id) {
878 let (Some(top), Some(start)) = (self.rect(target), self.rect(scroller)) else {
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;
909 self.scrolls.push((scroller, target));
885910 }
886911
887912 /// The pointer's cursor, or None over a custom box, whose host chooses.
......@@ -981,7 +1006,7 @@ impl Ui {
9811006 self.focus_ring = false;
9821007 if !self.popups.is_empty() {
9831008 let under = self.popups.iter().rposition(|popup| {
984 self.rect(popup.id)
1009 self.laid_out(popup.id)
9851010 .is_some_and(|rect| contains(rect, point))
9861011 });
9871012 // A press outside every popup only dismisses them.
......@@ -1385,6 +1410,15 @@ impl Ui {
13851410 state.scroll = state.scroll.clamp(0.0, most);
13861411 }
13871412 }
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 }
13881422 self.states
13891423 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);
13901424 self.texts.prune(self.frame);
......@@ -1415,14 +1449,19 @@ impl Ui {
14151449 let Some(anchor) = node.anchor else {
14161450 continue;
14171451 };
1418 let (id, rect) = (node.id, node.rect);
1419 if Some(id) == tip {
1452 let placed = Placed {
1453 id: node.id,
1454 anchor,
1455 around: node.around,
1456 rect: node.rect,
1457 };
1458 if Some(placed.id) == tip {
14201459 continue;
14211460 }
1422 if let Some(open) = self.opening(id, anchor) {
1461 if let Some(open) = self.opening(placed.id, anchor) {
14231462 self.scrim(anchor, open);
14241463 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));
14261465 }
14271466 let from = self.display.len() - self.popups_painted;
14281467 self.paint(index, None, None);
......@@ -1435,15 +1474,10 @@ impl Ui {
14351474 Display::Round(None),
14361475 ]);
14371476 }
1438 painted.push((
1439 id,
1440 anchor,
1441 rect,
1442 from..self.display.len() - self.popups_painted,
1443 ));
1477 painted.push((placed, from..self.display.len() - self.popups_painted));
14441478 }
1445 for (id, anchor, rect, range) in std::mem::replace(&mut self.painted, painted) {
1446 if self.painted.iter().any(|(shown, ..)| *shown == id) {
1479 for (placed, range) in std::mem::replace(&mut self.painted, painted) {
1480 if self.painted.iter().any(|(shown, _)| shown.id == placed.id) {
14471481 continue;
14481482 }
14491483 // What held still while it opened goes at once, uncovering what it stood in for.
......@@ -1460,18 +1494,17 @@ impl Ui {
14601494 .cloned()
14611495 .collect();
14621496 self.closing.push(Closing {
1463 id,
1497 placed,
14641498 display,
1465 anchor,
1466 rect,
14671499 closed: self.now,
14681500 });
14691501 }
14701502 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);
14721505 let open = match motion {
14731506 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)
14751508 }
14761509 PopupMotion::Cut => 0.0,
14771510 PopupMotion::Fade([_, close]) => {
......@@ -1481,13 +1514,13 @@ impl Ui {
14811514 if open == 0.0 {
14821515 continue;
14831516 }
1484 self.scrim(closing.anchor, open);
1517 self.scrim(anchor, open);
14851518 let grows = motion == PopupMotion::Grow;
1486 let motion = closing.anchor.motion(closing.rect, open, grows);
1519 let motion = closing.placed.motion(open, grows);
14871520 self.display.push(Display::Motion(Some(motion)));
14881521 self.display.extend(closing.display.iter().cloned());
14891522 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) {
14911524 self.closing.push(closing);
14921525 }
14931526 }
......@@ -2050,6 +2083,7 @@ impl Built {
20502083 offset: spec.offset,
20512084 cursor: spec.cursor,
20522085 anchor: spec.anchor,
2086 around: [0.0; 4],
20532087 open: 1.0,
20542088 motion: None,
20552089 marks: Vec::new(),
crates/ui/src/list.rs+1-1
......@@ -96,7 +96,7 @@ pub fn list<R: Rows>(
9696 }
9797 low
9898 };
99 let rect = ui.rect(id);
99 let rect = ui.laid_out(id);
100100 let view = f64::from(match spec.size[1].size {
101101 Size::Pixels(pixels) => pixels,
102102 _ => rect.map_or(0.0, |rect| rect[3] - rect[1]),
crates/ui/src/popup.rs+26-51
......@@ -117,7 +117,7 @@ pub fn menu(
117117 .map_or(0.0, |badge| crate::badge_width(ui, badge) + ICON_GAP)
118118 })
119119 .collect();
120 let window = ui.rect(Id::ROOT).map_or(0.0, |window| window[3]);
120 let window = ui.size()[1];
121121 let field = if filter.is_some() {
122122 ROW + style.pad
123123 } else {
......@@ -154,11 +154,6 @@ pub fn menu(
154154 0.0
155155 }
156156 + 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 };
162157 choose(
163158 ui,
164159 id,
......@@ -166,7 +161,7 @@ pub fn menu(
166161 anchor,
167162 &[("", items)],
168163 filter,
169 width.max(least).max(NARROWEST),
164 width.max(NARROWEST),
170165 style.row,
171166 )
172167 .map(|(_, index)| index)
......@@ -194,7 +189,7 @@ pub fn submenus(ui: &mut Ui, id: Id, items: &[Item], submenu: impl Fn(usize) ->
194189 highlight.is_some_and(opens) && !navigation(ui, &[id], &[NamedKey::ArrowRight]).is_empty();
195190 let under = highlight.filter(|key| {
196191 ui.pointer
197 .zip(ui.rect(rows.child(*key)))
192 .zip(ui.laid_out(rows.child(*key)))
198193 .is_some_and(|(point, rect)| crate::contains(rect, point))
199194 });
200195 let now = ui.now;
......@@ -225,23 +220,23 @@ pub fn submenus(ui: &mut Ui, id: Id, items: &[Item], submenu: impl Fn(usize) ->
225220}
226221
227222/// 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 a
229/// delay, or at once while another has just shown. A press or the wheel hides it until the
230/// pointer leaves. It names the box to assistive technology, or where the box has no role,
231/// the unnamed controls inside it.
223/// below the box built last while the pointer rests on it or on a box inside it:
224/// after a delay, or at once while another has just shown. A press or the wheel hides it
225/// until the pointer leaves. It names the box to assistive technology, or where the box has
226/// no role, the unnamed controls inside it.
232227pub fn tooltip(ui: &mut Ui, title: &str, keys: &str, description: Option<&str>) {
233228 tooltip_below(ui, None, title, keys, description);
234229}
235230
236/// A `tooltip` below `part`, a window rectangle within the box built last, as a custom
237/// box names what it draws.
238pub fn tooltip_over(ui: &mut Ui, part: [f32; 4], title: &str, description: Option<&str>) {
231/// A `tooltip` below `part`, a box built within the box built last, as a custom box names
232/// part of what it draws.
233pub fn tooltip_over(ui: &mut Ui, part: Id, title: &str, description: Option<&str>) {
239234 tooltip_below(ui, Some(part), title, "", description);
240235}
241236
242237fn tooltip_below(
243238 ui: &mut Ui,
244 part: Option<[f32; 4]>,
239 part: Option<Id>,
245240 title: &str,
246241 keys: &str,
247242 description: Option<&str>,
......@@ -312,7 +307,6 @@ fn tooltip_below(
312307 } else {
313308 format!("{title} ({keys})")
314309 };
315 let [left, top, right, bottom] = part.or(ui.rect(id)).unwrap_or_default();
316310 let theme = &ui.theme;
317311 let spec = Spec {
318312 axis: Axis::Y,
......@@ -322,7 +316,7 @@ fn tooltip_below(
322316 radius: 4.0,
323317 pad: [8.0, 5.0],
324318 gap: 3.0,
325 anchor: Some(Anchor::Below([left, top, right, bottom + PAD])),
319 anchor: Some(Anchor::Below(part.unwrap_or(id))),
326320 ..Spec::default()
327321 };
328322 ui.open_as(id.child("tooltip"), spec);
......@@ -379,8 +373,7 @@ pub fn palette(ui: &mut Ui, id: Id, modes: &[(&str, &[Item])], placeholder: &str
379373 (ui.signals.get(&rows.child(*key))).is_some_and(|signal| signal.context.is_some())
380374 });
381375 let chord = chord(ui, id.child("filter"), "k");
382 let window = ui.rect(Id::ROOT).unwrap_or_default();
383 let width = PALETTE.min(window[2] - 8.0 * PAD).max(NARROWEST);
376 let width = PALETTE.min(ui.size()[0] - 8.0 * PAD).max(NARROWEST);
384377 if let Some((mode, index)) = choose(
385378 ui,
386379 id,
......@@ -461,9 +454,9 @@ fn choose(
461454 let theme = ui.theme.clone();
462455 let style = theme.menu();
463456 // Over a combo box, the field takes the box's place at once, widening with the popup.
464 let (flags, height) = match anchor {
465 Anchor::Over(rect) => (Flags::STILL, rect[3] - rect[1]),
466 _ => (Flags::default(), ROW),
457 let flags = match anchor {
458 Anchor::Over(_) => Flags::STILL,
459 _ => Flags::default(),
467460 };
468461 surface(ui, id, role, anchor, width);
469462 if let Some(placeholder) = filter {
......@@ -475,7 +468,7 @@ fn choose(
475468 placeholder,
476469 Spec {
477470 flags,
478 size: [fill(), px(height)],
471 size: [fill(), px(ROW)],
479472 fill: Some(theme.base),
480473 border: Some(theme.accent),
481474 radius: 4.0,
......@@ -512,7 +505,7 @@ fn choose(
512505 };
513506 first.map(|row| matches.key(row))
514507 });
515 let window = ui.rect(Id::ROOT).map_or(0.0, |window| window[3]);
508 let window = ui.size()[1];
516509 let field = if filter.is_some() {
517510 ROW + style.pad
518511 } else {
......@@ -1208,7 +1201,7 @@ pub fn color_picker(
12081201 let mut picked = state(ui, id).picked.unwrap_or_else(|| to_hsl(initial));
12091202 let pointer = ui.pointer();
12101203 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)?;
12121205 let [x, y] = pointer?;
12131206 Some([
12141207 ((x - left) / (right - left)).clamp(0.0, 1.0),
......@@ -1412,33 +1405,15 @@ fn from_hsl([hue, saturation, lightness]: [f32; 3]) -> [u8; 3] {
14121405 [red, green, blue].map(|channel| ((channel + base) * 255.0).round().clamp(0.0, 255.0) as u8)
14131406}
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.
14161410fn surface(ui: &mut Ui, id: Id, role: Role, anchor: Anchor, width: f32) {
14171411 let style = ui.theme.menu();
1418 let pad = style.pad;
1419 // Level with its row, past the menu's edges.
14201412 let beside = ui
14211413 .popups
14221414 .iter()
1423 .position(|popup| popup.id == id)
1424 .and_then(|at| {
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 };
1415 .find(|popup| popup.id == id)
1416 .and_then(|popup| popup.beside);
14421417 let spec = Spec {
14431418 axis: Axis::Y,
14441419 size: [px(width), children()],
......@@ -1446,9 +1421,9 @@ fn surface(ui: &mut Ui, id: Id, role: Role, anchor: Anchor, width: f32) {
14461421 border: Some(style.border),
14471422 shadow: Some(ui.theme.shadow),
14481423 radius: style.radius,
1449 pad: [pad; 2],
1450 gap: pad,
1451 anchor: Some(anchor),
1424 pad: [style.pad; 2],
1425 gap: style.pad,
1426 anchor: Some(beside.map_or(anchor, Anchor::Right)),
14521427 role: Some(role),
14531428 ..Spec::default()
14541429 };
crates/ui/src/shell.rs+25-10
......@@ -54,8 +54,9 @@ const EDGE: f32 = 20.0;
5454/// The row is as wide as the tabs, and gives up all of that where its own row lacks room.
5555/// Then the tabs scroll sideways under the wheel, either way it turns, fading out at the
5656/// 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.
58#[allow(clippy::too_many_arguments)]
57/// opens. A tab given `true` after its colour shows the unread dot in its leading pad. The tab
58/// `renaming` holds what its builder builds inside it, given the tab's height.
59#[allow(clippy::too_many_arguments, clippy::type_complexity)]
5960pub fn section_tabs(
6061 ui: &mut Ui,
6162 row: Id,
......@@ -63,6 +64,7 @@ pub fn section_tabs(
6364 active: usize,
6465 lit: Option<usize>,
6566 dragged: Option<Dragged>,
67 mut renaming: Option<(usize, &mut dyn FnMut(&mut Ui, f32))>,
6668 section: &Section,
6769 height: f32,
6870 fill: [f32; 4],
......@@ -83,7 +85,7 @@ pub fn section_tabs(
8385 .collect();
8486 // The last tab's slant and shadow reach past its box.
8587 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]);
8789 let most = (wide - view).max(0.0);
8890 let [scroll, shown, shift] = [row.child("scroll"), row.child("shown"), row.child("offset")];
8991 let held_value = |ui: &Ui, id: Id| ui.states.get(&id).and_then(|state| state.tween);
......@@ -190,7 +192,7 @@ pub fn section_tabs(
190192 let top = if index == active { section } else { &colors };
191193 let fill = mix(colors.tab, top.frame[0], open);
192194 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(
194196 ("tab", index),
195197 Spec {
196198 flags: Flags::CLICKABLE | Flags::FLOAT,
......@@ -215,6 +217,11 @@ pub fn section_tabs(
215217 ..Spec::default()
216218 },
217219 );
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);
218225 if unread {
219226 let position = [
220227 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
548555 if enabled && ui.signal(id).pressed {
549556 ui.open_popup(menu);
550557 }
551 Anchor::Below(ui.rect(id).unwrap_or_default())
558 Anchor::Below(id)
552559}
553560
554561/// A button showing `icon` and a menu arrow, one control that opens popup `menu`, a toggle
......@@ -593,15 +600,23 @@ pub fn menu_button(
593600 },
594601 );
595602 }
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 );
596615 ui.close();
597616 if ui.signal(id).pressed {
598617 ui.open_popup(menu);
599618 }
600 let [left, top, right, bottom] = ui.rect(id).unwrap_or_default();
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])
619 Anchor::Below(from)
605620}
606621
607622/// 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() {
6262 ui.close();
6363 ui.close();
6464 });
65 assert_eq!(ui.rect(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]));
65 assert_eq!(ui.laid_out(ids[0]), Some([0.0, 0.0, 280.0, 100.0]));
66 assert_eq!(ui.laid_out(ids[1]), Some([280.0, 0.0, 400.0, 100.0]));
6767}
6868
6969#[test]
......@@ -93,7 +93,7 @@ fn padding_wider_than_its_box_leaves_loose_children_empty_not_negative() {
9393 ui.close();
9494 ui.close();
9595 });
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]));
9797}
9898
9999#[test]
......@@ -128,8 +128,8 @@ fn children_sum_with_gaps_and_padding() {
128128 ui.close();
129129 ui.close();
130130 });
131 assert_eq!(ui.rect(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]));
131 assert_eq!(ui.laid_out(column.unwrap()), Some([0.0, 0.0, 58.0, 44.0]));
132 assert_eq!(ui.laid_out(second.unwrap()), Some([4.0, 18.0, 34.0, 38.0]));
133133}
134134
135135/// 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;
191191 }
192192 ui.close();
193193 });
194 ids.into_iter().map(|id| ui.rect(id)).collect()
194 ids.into_iter().map(|id| ui.laid_out(id)).collect()
195195}
196196
197197#[test]
......@@ -296,12 +296,12 @@ fn a_group_inside_a_box_sized_by_its_children_folds_with_the_row() {
296296 };
297297 let [outer, after] = ["outer", "later"].map(|part| Id::ROOT.child("row").child(part));
298298 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]));
300300 // 50 + 100 + 30 overflows 179, so the group folds before the later one of higher priority,
301301 // and the box around it narrows to 70.
302302 build(&mut ui, 179.0);
303 assert_eq!(ui.rect(outer), Some([0.0, 0.0, 70.0, 20.0]));
304 assert_eq!(ui.rect(after), Some([70.0, 0.0, 100.0, 20.0]));
303 assert_eq!(ui.laid_out(outer), Some([0.0, 0.0, 70.0, 20.0]));
304 assert_eq!(ui.laid_out(after), Some([70.0, 0.0, 100.0, 20.0]));
305305 assert_eq!(ui.narrowest(Id::ROOT.child("row")), Some(100.0));
306306}
307307
......@@ -340,7 +340,7 @@ fn a_box_sized_by_its_children_yields_what_they_yield() {
340340 ui.close();
341341 ui.close();
342342 });
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]));
344344}
345345
346346#[test]
......@@ -621,7 +621,7 @@ fn wheel_scrolls_within_the_content_and_clips_children() {
621621 200.0,
622622 "at once, as on the page"
623623 );
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]));
625625 let layers = ui.layers();
626626 assert!(matches!(
627627 layers[..],
......@@ -959,7 +959,7 @@ fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {
959959 build(&mut ui, &mut offset);
960960 build(&mut ui, &mut offset);
961961 let thumb = Id::ROOT.child("view").child("bar");
962 let rect = ui.rect(thumb).unwrap();
962 let rect = ui.laid_out(thumb).unwrap();
963963 assert_eq!(
964964 rect,
965965 [190.0, 4.0, 196.0, 61.0],
......@@ -978,7 +978,7 @@ fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {
978978 build(&mut ui, &mut offset);
979979 assert_eq!(offset, 1000.0);
980980 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]);
982982 ui.event(Event::PointerMoved([193.0, -500.0]));
983983 build(&mut ui, &mut offset);
984984 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() {
10101010 ui.close();
10111011 });
10121012 }
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();
10141014 assert_eq!(rect[3], 204.0);
10151015}
10161016
......@@ -1067,7 +1067,7 @@ fn system_scrollers_step_page_and_drag_by_the_parts_the_platform_paints() {
10671067 build(&mut ui, &mut offset);
10681068 let bar = Id::ROOT.child("view").child("bar");
10691069 assert_eq!(
1070 ui.rect(bar).unwrap(),
1070 ui.laid_out(bar).unwrap(),
10711071 [185.0, 0.0, 200.0, 200.0],
10721072 "the corner stays clear"
10731073 );
......@@ -1126,7 +1126,11 @@ fn menu_id() -> Id {
11261126
11271127/// Builds a frame with a button filling the window under the menu, and returns the
11281128/// 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>) {
11301134 let mut result = (Signal::default(), None);
11311135 frame(ui, |ui| {
11321136 let under = ui.leaf(
......@@ -1137,28 +1141,49 @@ fn menu_frame(ui: &mut Ui, anchor: Anchor, filter: Option<&str>) -> (Signal, Opt
11371141 ..Spec::default()
11381142 },
11391143 );
1144 let anchor = anchor(ui);
11401145 let chosen = popup::menu(ui, menu_id(), anchor, &items(), filter);
11411146 result = (under, chosen);
11421147 });
11431148 result
11441149}
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`.
11491174fn open_menu(filter: Option<&str>) -> Ui {
11501175 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1151 menu_frame(&mut ui, BELOW, filter);
1176 menu_frame(&mut ui, below, filter);
11521177 ui.open_popup(menu_id());
11531178 for _ in 0..40 {
1154 menu_frame(&mut ui, BELOW, filter);
1179 menu_frame(&mut ui, below, filter);
11551180 }
11561181 ui
11571182}
11581183
11591184/// The centre of the menu's row for `item`, from the latest layout.
11601185fn 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();
11621187 [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0]
11631188}
11641189
......@@ -1189,7 +1214,7 @@ fn tip_frame(ui: &mut Ui) -> [Option<[f32; 4]>; 2] {
11891214 tips[index] = id.child("tooltip");
11901215 }
11911216 });
1192 tips.map(|tip| ui.rect(tip))
1217 tips.map(|tip| ui.laid_out(tip))
11931218}
11941219
11951220/// 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() {
12411266 assert!(rest(&mut ui, 500)[0].is_some());
12421267}
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
12441277#[test]
12451278fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() {
12461279 let mut ui = open_menu(None);
1247 let rect = ui.rect(menu_id()).unwrap();
1280 let rect = ui.laid_out(menu_id()).unwrap();
12481281 assert_eq!(
12491282 [rect[0], rect[1]],
12501283 [20.0, 44.0],
......@@ -1252,17 +1285,19 @@ fn popups_open_beside_their_anchor_and_flip_to_stay_in_the_window() {
12521285 );
12531286 assert_eq!(rect[2] - rect[0], 140.0, "no narrower than a menu");
12541287 let height = rect[3] - rect[1];
1255 menu_frame(&mut ui, Anchor::Below([300.0, 260.0, 380.0, 280.0]), None);
1256 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]));
1289 menu_frame(&mut ui, corner, None);
1290 menu_frame(&mut ui, corner, None);
12571291 assert_eq!(
1258 ui.rect(menu_id()),
1292 ui.laid_out(menu_id()),
12591293 Some([240.0, 256.0 - height, 380.0, 256.0]),
12601294 "above, level with the anchor's far edge"
12611295 );
1262 menu_frame(&mut ui, Anchor::Right([350.0, 100.0, 390.0, 120.0]), None);
1263 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]));
1297 menu_frame(&mut ui, edge, None);
1298 menu_frame(&mut ui, edge, None);
12641299 assert_eq!(
1265 ui.rect(menu_id()),
1300 ui.laid_out(menu_id()),
12661301 Some([210.0, 96.0, 350.0, 96.0 + height]),
12671302 "a submenu that cannot fit to the right opens to the left, its first row level"
12681303 );
......@@ -1280,7 +1315,8 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
12801315 let narrow = [100.0, 300.0];
12811316 let menu = |ui: &mut Ui| {
12821317 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);
12841320 });
12851321 };
12861322 menu(&mut ui);
......@@ -1288,14 +1324,14 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
12881324 for _ in 0..40 {
12891325 menu(&mut ui);
12901326 }
1291 let rect = ui.rect(menu_id()).unwrap();
1327 let rect = ui.laid_out(menu_id()).unwrap();
12921328 assert!(inside(rect, narrow), "{rect:?} leaves the window's margin");
12931329 assert_eq!(
12941330 rect[2] - rect[0],
12951331 100.0 - 2.0 * POPUP_MARGIN,
12961332 "as wide as fits"
12971333 );
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();
12991335 assert!(row[2] <= rect[2], "its rows narrow with it");
13001336
13011337 // 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() {
13331369 for _ in 0..40 {
13341370 build(&mut ui);
13351371 }
1336 let rect = ui.rect(dialog).unwrap();
1372 let rect = ui.laid_out(dialog).unwrap();
13371373 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();
13391375 ui.event(Event::PointerMoved([200.0, 60.0]));
13401376 ui.event(Event::Wheel([0.0, -100.0]));
13411377 for _ in 0..40 {
13421378 build(&mut ui);
13431379 }
1344 let scrolled = ui.rect(dialog.child(0)).unwrap();
1380 let scrolled = ui.laid_out(dialog.child(0)).unwrap();
13451381 assert!(scrolled[1] < first[1], "the wheel scrolls it");
13461382 for layer in ui.layers() {
13471383 let Layer::Primitives(layer) = layer else {
......@@ -1361,32 +1397,32 @@ fn popups_too_large_for_a_narrow_window_shrink_inside_its_margin() {
13611397#[test]
13621398fn popups_show_at_once_by_their_anchor() {
13631399 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
1364 menu_frame(&mut ui, BELOW, None);
1400 menu_frame(&mut ui, below, None);
13651401 ui.open_popup(menu_id());
1366 menu_frame(&mut ui, BELOW, None);
1367 menu_frame(&mut ui, BELOW, None);
1402 menu_frame(&mut ui, below, None);
1403 menu_frame(&mut ui, below, None);
13681404 assert_eq!(
1369 ui.rect(menu_id()).unwrap()[1],
1405 ui.laid_out(menu_id()).unwrap()[1],
13701406 44.0,
13711407 "right under the anchor"
13721408 );
13731409 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);
13751411 ui.open_popup(menu_id());
1376 menu_frame(&mut ui, Anchor::Point([50.0, 50.0]), None);
1377 menu_frame(&mut ui, Anchor::Point([50.0, 50.0]), None);
1378 assert_eq!(ui.rect(menu_id()).unwrap()[..2], [50.0, 50.0]);
1412 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1413 menu_frame(&mut ui, |_| Anchor::Point([50.0, 50.0]), None);
1414 assert_eq!(ui.laid_out(menu_id()).unwrap()[..2], [50.0, 50.0]);
13791415}
13801416
13811417#[test]
13821418fn a_press_outside_dismisses_without_reaching_what_is_beneath() {
13831419 let mut ui = open_menu(None);
13841420 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);
13861422 assert!(!ui.popup_open(menu_id()) && chosen.is_none());
13871423 assert!(!under.pressed && !under.clicked);
13881424 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);
13901426 assert!(under.clicked, "once closed, beneath takes presses again");
13911427}
13921428
......@@ -1394,13 +1430,13 @@ fn a_press_outside_dismisses_without_reaching_what_is_beneath() {
13941430fn escape_dismisses_and_returns_the_focus() {
13951431 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
13961432 let page = Id::ROOT.child("under");
1397 menu_frame(&mut ui, BELOW, None);
1433 menu_frame(&mut ui, below, None);
13981434 ui.set_focus(Some(page));
13991435 ui.open_popup(menu_id());
1400 menu_frame(&mut ui, BELOW, None);
1436 menu_frame(&mut ui, below, None);
14011437 assert_eq!(ui.focused(), Some(menu_id()));
14021438 ui.event(key(NamedKey::Escape));
1403 menu_frame(&mut ui, BELOW, None);
1439 menu_frame(&mut ui, below, None);
14041440 assert!(!ui.popup_open(menu_id()));
14051441 assert_eq!(ui.focused(), Some(page));
14061442}
......@@ -1409,11 +1445,11 @@ fn escape_dismisses_and_returns_the_focus() {
14091445fn input_over_a_popup_stays_with_it() {
14101446 let mut ui = open_menu(None);
14111447 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);
14131449 assert!(!under.hovered);
14141450 let point = row(&ui, 1);
14151451 click(&mut ui, point);
1416 let (under, chosen) = menu_frame(&mut ui, BELOW, None);
1452 let (under, chosen) = menu_frame(&mut ui, below, None);
14171453 assert_eq!(chosen, Some(1));
14181454 assert!(!under.pressed && !under.clicked);
14191455 assert!(!ui.popup_open(menu_id()), "choosing closes it");
......@@ -1424,7 +1460,7 @@ fn disabled_items_ignore_the_pointer() {
14241460 let mut ui = open_menu(None);
14251461 let point = row(&ui, 2);
14261462 click(&mut ui, point);
1427 let (_, chosen) = menu_frame(&mut ui, BELOW, None);
1463 let (_, chosen) = menu_frame(&mut ui, below, None);
14281464 assert_eq!(chosen, None);
14291465 assert!(ui.popup_open(menu_id()));
14301466}
......@@ -1444,21 +1480,21 @@ fn keys_move_the_highlight_past_disabled_items() {
14441480 for named in keys {
14451481 ui.event(key(named));
14461482 }
1447 menu_frame(&mut ui, BELOW, None);
1483 menu_frame(&mut ui, below, None);
14481484 assert_eq!(ui.popups[0].highlight, Some(highlight));
14491485 }
14501486 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));
14521488}
14531489
14541490#[test]
14551491fn the_pointer_moves_the_highlight_only_when_it_moves() {
14561492 let mut ui = open_menu(None);
14571493 ui.event(Event::PointerMoved(row(&ui, 1)));
1458 menu_frame(&mut ui, BELOW, None);
1494 menu_frame(&mut ui, below, None);
14591495 ui.event(key(NamedKey::ArrowDown));
1460 menu_frame(&mut ui, BELOW, None);
1461 menu_frame(&mut ui, BELOW, None);
1496 menu_frame(&mut ui, below, None);
1497 menu_frame(&mut ui, below, None);
14621498 assert_eq!(
14631499 ui.popups[0].highlight,
14641500 Some(3),
......@@ -1483,7 +1519,8 @@ fn headings_are_never_chosen_and_hide_while_filtered() {
14831519 let build = |ui: &mut Ui| {
14841520 let mut chosen = None;
14851521 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"));
14871524 });
14881525 chosen
14891526 };
......@@ -1516,7 +1553,7 @@ fn typing_filters_to_the_best_matches_first() {
15161553 }
15171554 let settle = |ui: &mut Ui| {
15181555 for _ in 0..30 {
1519 menu_frame(ui, BELOW, Some("Filter"));
1556 menu_frame(ui, below, Some("Filter"));
15201557 }
15211558 };
15221559 settle(&mut ui);
......@@ -1524,7 +1561,7 @@ fn typing_filters_to_the_best_matches_first() {
15241561 let shown = |ui: &Ui| {
15251562 let mut shown: Vec<_> = (0..4_usize)
15261563 .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))?;
15281565 Some((rect[1] as i32, item))
15291566 })
15301567 .collect();
......@@ -1541,20 +1578,20 @@ fn typing_filters_to_the_best_matches_first() {
15411578 assert_eq!(shown(&ui), [0, 1, 3], "equal matches keep their order");
15421579 ui.event(key(NamedKey::ArrowDown));
15431580 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));
15451582}
15461583
15471584#[test]
15481585fn nothing_matching_leaves_nothing_to_choose_and_enter_leaves_the_menu() {
15491586 let mut ui = open_menu(Some("Filter"));
15501587 ui.event(typed("z"));
1551 menu_frame(&mut ui, BELOW, Some("Filter"));
1588 menu_frame(&mut ui, below, Some("Filter"));
15521589 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"));
15541591 assert_eq!(chosen, None);
15551592 assert!(ui.popup_open(menu_id()));
15561593 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"));
15581595 assert_eq!(chosen, None);
15591596 assert!(!ui.popup_open(menu_id()));
15601597}
......@@ -1579,7 +1616,7 @@ fn the_palette_centres_near_the_windows_top_and_chooses_the_best_match() {
15791616 for _ in 0..40 {
15801617 build(&mut ui);
15811618 }
1582 let rect = ui.rect(palette).unwrap();
1619 let rect = ui.laid_out(palette).unwrap();
15831620 assert_eq!(rect[0] + rect[2], 400.0, "centred");
15841621 assert_eq!(rect[1], 300.0 / 8.0, "an eighth down");
15851622 ui.event(typed("a"));
......@@ -1704,7 +1741,7 @@ fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {
17041741 ui.popup_open(palette) && !ui.popup_open(actions),
17051742 "Escape goes back to the palette"
17061743 );
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();
17081745 ui.event(Event::PointerMoved([row[0] + 4.0, row[1] + 4.0]));
17091746 ui.event(Event::Button {
17101747 button: MouseButton::Right,
......@@ -1718,7 +1755,7 @@ fn the_palette_opens_an_items_actions_on_command_k_or_a_right_click() {
17181755 ui.event(typed("c"));
17191756 build(&mut ui);
17201757 assert_eq!(
1721 ui.rect(palette.child("rows").child(0_u64)),
1758 ui.laid_out(palette.child("rows").child(0_u64)),
17221759 None,
17231760 "a repeated item shows only while unfiltered"
17241761 );
......@@ -1734,7 +1771,8 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {
17341771 let build = |ui: &mut Ui| {
17351772 let mut chosen = None;
17361773 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);
17381776 });
17391777 chosen
17401778 };
......@@ -1755,7 +1793,7 @@ fn colour_grids_move_in_two_dimensions_and_choose_a_swatch_or_none() {
17551793 for _ in 0..40 {
17561794 build(&mut ui);
17571795 }
1758 let rect = ui.rect(grid.child(("cell", 0_usize))).unwrap();
1796 let rect = ui.laid_out(grid.child(("cell", 0_usize))).unwrap();
17591797 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
17601798 assert_eq!(build(&mut ui), Some(None));
17611799}
......@@ -1767,7 +1805,8 @@ fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {
17671805 let build = |ui: &mut Ui| {
17681806 let mut chosen = None;
17691807 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]);
17711810 });
17721811 chosen
17731812 };
......@@ -1789,7 +1828,7 @@ fn table_pickers_choose_columns_and_rows_by_keys_or_a_click() {
17891828 for _ in 0..40 {
17901829 build(&mut ui);
17911830 }
1792 let rect = ui.rect(grid.child(("cell", 5_usize))).unwrap();
1831 let rect = ui.laid_out(grid.child(("cell", 5_usize))).unwrap();
17931832 click(&mut ui, [rect[0] + 4.0, rect[1] + 4.0]);
17941833 assert_eq!(build(&mut ui), Some([2, 2]));
17951834}
......@@ -1804,7 +1843,7 @@ fn popups_opened_within_a_popup_stay_above_it() {
18041843 menu_id(),
18051844 Spec {
18061845 size: [px(100.0), px(100.0)],
1807 anchor: Some(BELOW),
1846 anchor: Some(Anchor::Point([20.0, 44.0])),
18081847 ..Spec::default()
18091848 },
18101849 );
......@@ -1812,13 +1851,8 @@ fn popups_opened_within_a_popup_stay_above_it() {
18121851 ui.open_popup(submenu);
18131852 }
18141853 ui.close();
1815 popup::menu(
1816 ui,
1817 submenu,
1818 Anchor::Right([100.0, 50.0, 120.0, 70.0]),
1819 &items(),
1820 None,
1821 );
1854 let anchor = Anchor::Right(spot(ui, [100.0, 50.0, 120.0, 70.0]));
1855 popup::menu(ui, submenu, anchor, &items(), None);
18221856 })
18231857 };
18241858 build(&mut ui, true);
......@@ -1837,11 +1871,12 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho
18371871 let build = |ui: &mut Ui| {
18381872 let mut chosen = None;
18391873 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);
18411876 popup::submenus(ui, menu_id(), &entries, |item| {
18421877 (item == 1).then_some(submenu)
18431878 });
1844 popup::menu(ui, submenu, BELOW, &items(), None);
1879 popup::menu(ui, submenu, anchor, &items(), None);
18451880 });
18461881 chosen
18471882 };
......@@ -1860,7 +1895,7 @@ fn a_submenu_opens_beside_its_row_once_the_pointer_rests_and_closes_with_its_cho
18601895 build(&mut ui);
18611896 }
18621897 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());
18641899 assert!(
18651900 beside[0] >= menu[2] - 1.0,
18661901 "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
18781913 assert_eq!(build(&mut ui), None, "a click opens it, not a choice");
18791914 build(&mut ui);
18801915 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();
18821917 click(&mut ui, [first[0] + 4.0, first[1] + 4.0]);
18831918 build(&mut ui);
18841919 assert!(
......@@ -1967,7 +2002,7 @@ fn list_frame(
19672002}
19682003
19692004fn 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])
19712006}
19722007
19732008/// Frames until the list's animations end.
......@@ -2071,7 +2106,8 @@ fn a_menu_runs_to_the_window_edge_before_it_scrolls() {
20712106 .collect();
20722107 let build = |ui: &mut Ui, height: f32| {
20732108 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);
20752111 })
20762112 };
20772113 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
......@@ -2080,9 +2116,9 @@ fn a_menu_runs_to_the_window_edge_before_it_scrolls() {
20802116 for _ in 0..3 {
20812117 build(&mut ui, 800.0);
20822118 }
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"));
20842120 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());
20862122 for _ in 0..3 {
20872123 build(&mut ui, 300.0);
20882124 }
......@@ -2101,7 +2137,8 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {
21012137 .collect();
21022138 let build = |ui: &mut Ui| {
21032139 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);
21052142 })
21062143 };
21072144 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
......@@ -2109,9 +2146,9 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {
21092146 ui.open_popup(menu_id());
21102147 build(&mut ui);
21112148 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();
21132150 let top = ui
2114 .rect(menu_id().child("rows").child(0u64))
2151 .laid_out(menu_id().child("rows").child(0u64))
21152152 .map(|rect| rect[1]);
21162153 let at = Instant::now();
21172154 let x = (thumb[0] + thumb[2]) / 2.0;
......@@ -2123,7 +2160,7 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() {
21232160 build(&mut ui);
21242161 assert!(ui.popup_open(menu_id()));
21252162 assert_ne!(
2126 ui.rect(menu_id().child("rows").child(0u64))
2163 ui.laid_out(menu_id().child("rows").child(0u64))
21272164 .map(|rect| rect[1]),
21282165 top
21292166 );
......@@ -2141,7 +2178,8 @@ fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end(
21412178 .collect();
21422179 let build = |ui: &mut Ui| {
21432180 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);
21452183 })
21462184 };
21472185 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(
21522190 build(&mut ui);
21532191 build(&mut ui);
21542192 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();
21562194 let x = (thumb[0] + thumb[2]) / 2.0;
21572195 ui.event(Event::PointerMoved([x, thumb[1] + 2.0]));
21582196 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(
21612199 build(&mut ui);
21622200 build(&mut ui);
21632201 assert_eq!(fade(&ui), [12.0, 0.0], "cut only at the top");
2164 let list = ui.rect(rows).unwrap();
2165 let thumb = ui.rect(rows.child("bar")).unwrap();
2202 let list = ui.laid_out(rows).unwrap();
2203 let thumb = ui.laid_out(rows.child("bar")).unwrap();
21662204 assert_eq!(
21672205 thumb[3],
21682206 list[3] - 4.0,
......@@ -2187,7 +2225,8 @@ fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() {
21872225 .collect();
21882226 let build = |ui: &mut Ui| {
21892227 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);
21912230 })
21922231 };
21932232 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() {
21972236 build(&mut ui);
21982237 }
21992238 assert_eq!(ui.popups[0].highlight, picker.then_some(30));
2200 let top = ui.rect(rows).unwrap()[1];
2201 let first = ui.rect(rows.child(0u64)).map(|rect| rect[1]);
2239 let top = ui.laid_out(rows).unwrap()[1];
2240 let first = ui.laid_out(rows.child(0u64)).map(|rect| rect[1]);
22022241 assert_eq!(first == Some(top), !picker, "{first:?} under {top}");
22032242 }
22042243}
......@@ -2347,7 +2386,8 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {
23472386 let fonts = fonts();
23482387 let build = |ui: &mut Ui| {
23492388 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"));
23512391 })
23522392 };
23532393 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() {
23582398 }
23592399 let results = menu_id().child("rows");
23602400 let height = |ui: &Ui| {
2361 let rect = ui.rect(results).unwrap();
2401 let rect = ui.laid_out(results).unwrap();
23622402 rect[3] - rect[1]
23632403 };
23642404 assert!(
......@@ -2373,15 +2413,18 @@ fn a_filtered_menu_shows_its_new_results_at_once_from_the_top() {
23732413 for query in ["t", "nr"] {
23742414 ui.event(typed(query));
23752415 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))];
23772417 build(&mut ui);
23782418 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 );
23802423 }
2381 let row = ui.rect(results.child(42_u64)).unwrap();
2424 let row = ui.laid_out(results.child(42_u64)).unwrap();
23822425 assert_eq!(
23832426 row[1],
2384 ui.rect(results).unwrap()[1],
2427 ui.laid_out(results).unwrap()[1],
23852428 "Times New Roman at the top"
23862429 );
23872430 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() {
25992642
26002643/// Clicks the middle of `id`'s box as laid out now, where it must be hit.
26012644fn click_box(ui: &mut Ui, id: Id) {
2602 let rect = ui.rect(id).unwrap();
2645 let rect = ui.laid_out(id).unwrap();
26032646 let middle = [(rect[0] + rect[2]) / 2.0, (rect[1] + rect[3]) / 2.0];
26042647 assert_eq!(
26052648 ui.box_at(middle),
......@@ -2662,7 +2705,7 @@ fn dialog_frame(ui: &mut Ui, text: &mut String) -> Reported {
26622705 text,
26632706 ..popup::Item::default()
26642707 });
2665 let anchor = Anchor::Below(ui.rect(combo).unwrap_or_default());
2708 let anchor = Anchor::Below(combo);
26662709 reported.scheme = popup::menu(ui, Id::ROOT.child("schemes"), anchor, &items, None);
26672710 ui.close();
26682711 });
......@@ -2722,8 +2765,7 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl
27222765 );
27232766 shell::combo(ui, "combo", "Font", "Calibri", 120.0, menu_id(), true);
27242767 ui.close();
2725 let anchor = Anchor::Over(ui.rect(combo).unwrap_or_default());
2726 chosen = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"));
2768 chosen = popup::menu(ui, menu_id(), Anchor::Over(combo), &fonts, Some("Font"));
27272769 });
27282770 chosen
27292771 };
......@@ -2731,12 +2773,12 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl
27312773 ui.open_popup(menu_id());
27322774 build(&mut ui);
27332775 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()];
27352777 assert_eq!(first[..2], box_rect[..2], "the field starts on the box");
27362778 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();
27382780 let row = menu_id().child("rows").child(3_u64);
2739 let opening = ui.rect(row).unwrap();
2781 let opening = ui.laid_out(row).unwrap();
27402782 assert!(
27412783 opening[2] > shown[2],
27422784 "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
27702812 build(&mut ui);
27712813 }
27722814 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()];
27742816 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");
27762818 click_box(&mut ui, menu_id().child("rows").child(3_u64));
27772819 assert_eq!(build(&mut ui), Some(3));
27782820}
......@@ -2781,9 +2823,9 @@ fn a_combo_s_field_widens_over_rows_laid_out_where_they_end_and_its_rows_take_cl
27812823fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
27822824 let mut ui = Ui::new(Theme::light(), DOUBLE_CLICK);
27832825 let fonts = fonts();
2784 let anchor = Anchor::Over([330.0, 10.0, 390.0, 30.0]);
27852826 let build = |ui: &mut Ui| {
27862827 frame(ui, |ui| {
2828 let anchor = Anchor::Over(spot(ui, [330.0, 10.0, 390.0, 30.0]));
27872829 _ = popup::menu(ui, menu_id(), anchor, &fonts, Some("Font"))
27882830 })
27892831 };
......@@ -2791,11 +2833,12 @@ fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
27912833 ui.open_popup(menu_id());
27922834 build(&mut ui);
27932835 let field = menu_id().child("filter");
2794 let edges =
2795 |ui: &Ui| [ui.rect(field).unwrap(), ui.rect(menu_id()).unwrap()].map(|rect| rect[2]);
2796 let first = ui.rect(field).unwrap();
2836 let edges = |ui: &Ui| {
2837 [ui.laid_out(field).unwrap(), ui.laid_out(menu_id()).unwrap()].map(|rect| rect[2])
2838 };
2839 let first = ui.laid_out(field).unwrap();
27972840 let row = menu_id().child("rows").child(3_u64);
2798 let opening = ui.rect(row).unwrap();
2841 let opening = ui.laid_out(row).unwrap();
27992842 assert_eq!(
28002843 edges(&ui),
28012844 [390.0, 394.0],
......@@ -2807,10 +2850,10 @@ fn a_popup_over_a_box_by_the_window_s_edge_widens_away_from_it() {
28072850 }
28082851 assert_eq!(edges(&ui), [390.0, 394.0], "the trailing edges hold");
28092852 assert!(
2810 ui.rect(field).unwrap()[0] < first[0] - 30.0,
2853 ui.laid_out(field).unwrap()[0] < first[0] - 30.0,
28112854 "the field widens leftward"
28122855 );
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");
28142857}
28152858
28162859fn built(ui: &Ui, id: Id) -> &Built {
......@@ -2835,7 +2878,8 @@ fn split_frame(ui: &mut Ui) -> Signal {
28352878 text: "Item",
28362879 ..Default::default()
28372880 };
2838 popup::menu(ui, split_menu(), BELOW, &[item], None);
2881 let anchor = below(ui);
2882 popup::menu(ui, split_menu(), anchor, &[item], None);
28392883 ui.close();
28402884 });
28412885 signal
......@@ -2867,24 +2911,24 @@ fn a_split_button_fills_its_button_alone_and_outlines_both_halves_from_its_arrow
28672911 split_frame(ui);
28682912 }
28692913 };
2870 ui.event(Event::PointerMoved(center(ui.rect(button).unwrap())));
2914 ui.event(Event::PointerMoved(center(ui.laid_out(button).unwrap())));
28712915 settle(&mut ui);
28722916 // 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();
28742918 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]));
28762920 assert!(built(&ui, button).flags.contains(Flags::CLIP));
28772921 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())));
28792923 settle(&mut ui);
28802924 assert!(built(&ui, face).fill.is_none());
28812925 assert!(built(&ui, ring).border.is_some());
28822926 assert_eq!(
2883 ui.rect(ring),
2884 Some([left, top, ui.rect(arrow).unwrap()[2], bottom])
2927 ui.laid_out(ring),
2928 Some([left, top, ui.laid_out(arrow).unwrap()[2], bottom])
28852929 );
28862930 // 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());
28882932 click(&mut ui, point);
28892933 assert!(!split_frame(&mut ui).clicked);
28902934 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
28932937 assert!(built(&ui, ring).border.is_some());
28942938 ui.close_popup(split_menu());
28952939 settle(&mut ui);
2896 let point = center(ui.rect(button).unwrap());
2940 let point = center(ui.laid_out(button).unwrap());
28972941 click(&mut ui, point);
28982942 assert!(split_frame(&mut ui).clicked);
28992943 assert!(!ui.popup_open(split_menu()));
......@@ -2932,7 +2976,7 @@ fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {
29322976 });
29332977 };
29342978 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();
29362980 // One target: the arrow opens the menu as the icon does.
29372981 click(&mut ui, [button[2] - 3.0, center(button)[1]]);
29382982 for _ in 0..40 {
......@@ -2940,7 +2984,7 @@ fn a_menu_button_opens_its_menu_with_the_icons_under_its_own() {
29402984 }
29412985 assert!(ui.popup_open(menu_id()));
29422986 let icon = ui
2943 .rect(
2987 .laid_out(
29442988 menu_id()
29452989 .child("rows")
29462990 .child(0_usize)
......@@ -2965,7 +3009,8 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() {
29653009 let build = |ui: &mut Ui| {
29663010 let mut chosen = None;
29673011 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| {
29693014 ui.leaf(
29703015 "label",
29713016 Spec {
......@@ -2990,7 +3035,7 @@ fn galleries_choose_across_their_groups_by_keys_and_clicks() {
29903035 for _ in 0..40 {
29913036 build(&mut ui);
29923037 }
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();
29943039 // The library starts a row of its own under its heading.
29953040 assert_eq!(cell(2)[0], cell(0)[0]);
29963041 assert!(cell(2)[1] > cell(0)[3]);
......@@ -3007,7 +3052,7 @@ fn desktop_menus_cut_or_fade_without_growing() {
30073052 Layer::Custom { .. } => None,
30083053 })
30093054 };
3010 let at = Anchor::Point([50.0, 50.0]);
3055 let at = |_: &mut Ui| Anchor::Point([50.0, 50.0]);
30113056 let styled = |motion| {
30123057 let mut theme = Theme::light();
30133058 theme.desktop_menu = Some(Menu {
......@@ -3066,7 +3111,9 @@ fn tab_row(ui: &mut Ui, active: usize) -> Id {
30663111 ..Spec::default()
30673112 },
30683113 );
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 );
30703117 ui.close();
30713118 });
30723119 row
......@@ -3085,10 +3132,10 @@ fn settle_tabs(ui: &mut Ui, active: usize) -> Id {
30853132fn overflowing_tabs_scroll_the_open_one_into_view() {
30863133 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
30873134 let row = settle_tabs(&mut ui, 0);
3088 let view = ui.rect(row).unwrap();
3135 let view = ui.laid_out(row).unwrap();
30893136 assert_eq!(view[2] - view[0], 200.0);
30903137 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();
30923139 assert!(
30933140 last[0] >= view[0] && last[2] <= view[2],
30943141 "{last:?} in {view:?}"
......@@ -3099,17 +3146,17 @@ fn overflowing_tabs_scroll_the_open_one_into_view() {
30993146fn the_wheel_scrolls_overflowing_tabs_sideways_either_way_it_turns() {
31003147 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
31013148 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];
31033150 ui.event(Event::PointerMoved([100.0, 14.0]));
31043151 ui.event(Event::Wheel([0.0, -40.0]));
31053152 tab_row(&mut ui, 0);
31063153 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];
31083155 assert_eq!(first - scrolled, 40.0, "at once, as on the page");
31093156 ui.event(Event::Wheel([30.0, 0.0]));
31103157 tab_row(&mut ui, 0);
31113158 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];
31133160 assert_eq!(back - scrolled, 30.0);
31143161}
31153162
......@@ -3120,10 +3167,11 @@ fn a_colour_picker_applies_its_colour_and_keeps_it_through_hsl() {
31203167 let build = |ui: &mut Ui| {
31213168 let mut chosen = None;
31223169 frame(ui, |ui| {
3170 let anchor = below(ui);
31233171 chosen = popup::color_picker(
31243172 ui,
31253173 picker,
3126 BELOW,
3174 anchor,
31273175 "Custom Color",
31283176 [0xd4, 0xf9, 0xf2],
31293177 |_| [1.0; 4],
......@@ -3246,7 +3294,7 @@ fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_
32463294 ui.close();
32473295 });
32483296 };
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();
32503298 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
32513299 build(&mut ui, 2);
32523300 ui.open_popup(dialog);
......@@ -3271,7 +3319,7 @@ fn a_loose_dialog_fits_its_contents_to_the_window_and_its_side_stretches_to_its_
32713319 for _ in 0..60 {
32723320 build(&mut ui, 20);
32733321 }
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]);
32753323 assert!(
32763324 (row - top).abs() < 0.5,
32773325 "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() {
33163364 ..Spec::default()
33173365 },
33183366 );
3319 ui.rect(Id::ROOT.child("new"))
3367 ui.laid_out(Id::ROOT.child("new"))
33203368 };
33213369 let mut seen = Vec::new();
33223370 frame(&mut ui, |ui| seen.push(build(ui)));
crates/ui/src/widgets.rs+3-3
......@@ -360,7 +360,7 @@ pub fn scrollbar(
360360 return system_scrollbar(ui, part, axis, offset, range, view, corner);
361361 }
362362 let along = usize::from(axis == Axis::Y);
363 let rect = ui.rect(ui.current())?;
363 let rect = ui.laid_out(ui.current())?;
364364 let length = rect[along + 2] - rect[along];
365365 let cross = rect[3 - along] - rect[1 - along];
366366 let span = range[1] - range[0];
......@@ -415,7 +415,7 @@ fn system_scrollbar(
415415 corner: bool,
416416) -> Option<f32> {
417417 let along = usize::from(axis == Axis::Y);
418 let rect = ui.rect(ui.current())?;
418 let rect = ui.laid_out(ui.current())?;
419419 let span = range[1] - range[0];
420420 let thickness = ui.scrollers.as_ref()?.thickness;
421421 let length = rect[along + 2] - rect[along] - if corner { thickness } else { 0.0 };
......@@ -572,7 +572,7 @@ fn field(
572572 let modifiers = edit_modifiers(ui.modifiers());
573573 let pointer = ui
574574 .pointer()
575 .zip(ui.rect(id))
575 .zip(ui.laid_out(id))
576576 .map(|(pointer, rect)| pointer[0] - rect[0] - pad)
577577 .filter(|_| signal.pressed || signal.dragging);
578578 let state = ui.state(id);