From 76804506dc83a2821153953ccff390b6bd7dd451 Mon Sep 17 00:00:00 2001 From: JimKnoxx Date: Sun, 18 Oct 2026 10:52:57 +0200 Subject: [PATCH] M5: an open overlay follows the layer that opened it when the view changes Review follow-up. The overlay kept the screen rect its trigger had when it opened, so after a resize, a size option change or a pan a trigger-relative (or manual) overlay stayed where the trigger used to be. It now keeps that rect in page units of the presented frame and places it again on every layout. The dropdown test switches to Fill while the menu is open. The anchor's is_screen gets a doc comment. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/proto.rs | 1 + crates/ui-egui/src/present.rs | 20 ++++++++++++++----- .../src/present_overlay_anchor_tests.rs | 18 ++++++++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/crates/doc/src/proto.rs b/crates/doc/src/proto.rs index d3830bfb3..b4808011f 100644 --- a/crates/doc/src/proto.rs +++ b/crates/doc/src/proto.rs @@ -517,6 +517,7 @@ pub enum OverlayAnchor { } impl OverlayAnchor { + /// The default (the screen places the overlay), which files and JSON leave out. pub fn is_screen(&self) -> bool { *self == OverlayAnchor::Screen } diff --git a/crates/ui-egui/src/present.rs b/crates/ui-egui/src/present.rs index 1b483a2c2..13f4c53bf 100644 --- a/crates/ui-egui/src/present.rs +++ b/crates/ui-egui/src/present.rs @@ -67,8 +67,9 @@ pub struct OverlayState { pub position: OverlayPosition, pub close_on_outside: bool, pub background: Option, - /// Screen rect of the layer that opened it (manual position). - pub anchor: Option, + /// The layer that opened it, in page units of the presented frame (so it follows the frame + /// when the window resizes or the view pans). + pub anchor: Option, /// Manual position: the overlay's top-left from the anchor's (page units). pub offset: Option<[f64; 2]>, pub anchor_to: OverlayAnchor, @@ -790,6 +791,13 @@ impl Presenter { error } + /// Screen rect `r` in page units of the frame shown last. + fn page_rect(&self, r: Option) -> Option { + let (r, (_, place, _)) = (r?, self.shown?); + let (a, b) = (place.page(r.min), place.page(r.max)); + Some(figcraft_geom::Rect::new(a.x, a.y, b.x, b.y)) + } + fn perform(&mut self, a: &Action, node: NodeId, tr: Transition, dur: f64, easing: Easing, rect: Option, now: f64) -> bool { self.perform_at(a, node, tr, dur, easing, rect, now, 0) } @@ -833,6 +841,7 @@ impl Presenter { true } Action::OpenOverlay { dest, position, close_on_outside, background, offset, anchor } => { + let at = self.page_rect(rect); if !doc.contains(*dest) { return false; } @@ -842,7 +851,7 @@ impl Presenter { position: *position, close_on_outside: *close_on_outside, background: *background, - anchor: rect, + anchor: at, offset: *offset, anchor_to: *anchor, opened: now, @@ -854,6 +863,7 @@ impl Presenter { true } Action::SwapOverlay { dest } => { + let at = self.page_rect(rect); if !doc.contains(*dest) { return false; } @@ -871,7 +881,7 @@ impl Presenter { position: OverlayPosition::Center, close_on_outside: true, background: None, - anchor: rect, + anchor: at, offset: None, anchor_to: OverlayAnchor::Screen, opened: now, @@ -1647,7 +1657,7 @@ fn layout(p: &mut Presenter, area: Rect, chrome: Chrome) -> Option { let ob = disp.world_bounds(o.id)?; let size = vec2((ob.width() * scale) as f32, (ob.height() * scale) as f32); let offset = o.offset.map(|[x, y]| vec2((x * scale) as f32, (y * scale) as f32)); - let r = anchored_overlay_rect(o.position, o.anchor_to, screen, size, o.anchor, offset); + let r = anchored_overlay_rect(o.position, o.anchor_to, screen, size, o.anchor.map(|a| place.rect(a)), offset); Some((o.id, Place { page_min: (ob.x0, ob.y0), origin: r.min, scale }, r)) }; let overlays = p.overlays.iter().filter_map(place_overlay).collect(); diff --git a/crates/ui-egui/src/present_overlay_anchor_tests.rs b/crates/ui-egui/src/present_overlay_anchor_tests.rs index 1cb3e310b..16d16b2db 100644 --- a/crates/ui-egui/src/present_overlay_anchor_tests.rs +++ b/crates/ui-egui/src/present_overlay_anchor_tests.rs @@ -64,12 +64,24 @@ fn a_dropdown_opens_right_under_its_button() { let mut u = Harness::new(s); u.run("proto.present", json!({"start": a})); u.frames(2, Vec::new); + let scale = |u: &mut Harness, scale: &str| { + u.run("proto.presentOptions", json!({"scale": scale})); + u.frames(2, Vec::new); + }; + scale(&mut u, "fill"); + let filled = hotspot(&u, hot); + scale(&mut u, "fit"); let h = hotspot(&u, hot); + assert_ne!(filled, h, "fill shows the frame at another size"); u.frame(vec![Event::PointerMoved(h.center())]); u.frame(vec![button(h.center(), true)]); u.frame(vec![button(h.center(), false)]); u.frames(2, Vec::new); - let got = hotspot(&u, item).min; - let want = pos2(h.min.x, h.max.y); - assert!((got - want).length() < 0.01, "{got:?} vs {want:?}"); + let under = |u: &Harness, b: Rect| { + let (got, want) = (hotspot(u, item).min, pos2(b.min.x, b.max.y)); + assert!((got - want).length() < 0.01, "{got:?} vs {want:?}"); + }; + under(&u, h); + scale(&mut u, "fill"); + under(&u, filled); }