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 <noreply@anthropic.com>
This commit is contained in:
JimKnoxx
2026-10-24 09:19:33 +02:00
committed by Albert Nimtz
co-authored by Claude Opus 5.5
parent 545cd630fd
commit 76804506dc
3 changed files with 31 additions and 8 deletions
+1
View File
@@ -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
}
+15 -5
View File
@@ -67,8 +67,9 @@ pub struct OverlayState {
pub position: OverlayPosition,
pub close_on_outside: bool,
pub background: Option<Color>,
/// Screen rect of the layer that opened it (manual position).
pub anchor: Option<Rect>,
/// 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<figcraft_geom::Rect>,
/// 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<Rect>) -> Option<figcraft_geom::Rect> {
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<Rect>, 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<Layout> {
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();
@@ -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);
}