From 97318c188b4052efb0ec3bfb1255a6bba28ed1e9 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 07:24:41 +0700 Subject: [PATCH] Fix #862: the gradient stop popover opens beside the art Opened from a stop on the Gradient tool's annotator, the colour popover appeared just below and right of the stop, over the very art whose colour it changes. It now opens beside the selected art, on the first side with room for it (right, left, below, above), and beside the stop only when there is none. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/gradient_stop.rs | 63 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/dialogs/gradient_stop.rs b/crates/ui-egui/src/dialogs/gradient_stop.rs index 7282415a..d963311e 100644 --- a/crates/ui-egui/src/dialogs/gradient_stop.rs +++ b/crates/ui-egui/src/dialogs/gradient_stop.rs @@ -90,12 +90,19 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let screen = d.fields.get("screen").and_then(Value::as_array).and_then(|a| Some(egui::pos2(a.first()?.as_f64()? as f32, a.get(1)?.as_f64()? as f32))); let chip = Point::new(d.f64("x", 0.0), d.f64("y", 0.0)); - let pos = screen.unwrap_or_else(|| { - app.canvas_rect.zip(app.view().copied()).map_or(ctx.content_rect().center(), |(r, v)| Xf::new(r, &v).to_screen(chip) + vec2(14.0, 14.0)) + let area = egui::Id::new("gradient-stop-popover"); + let size = ctx.memory(|m| m.area_rect(area)).map_or(POPOVER_SIZE, |r| r.size()); + let pos = screen.unwrap_or_else(|| match app.canvas_rect.zip(app.view().copied()) { + Some((canvas, v)) => { + let xf = Xf::new(canvas, &v); + let at = xf.to_screen(chip); + selection_on_screen(app, &xf).map_or(at + vec2(14.0, 14.0), |art| beside_art(at, art, size, canvas)) + } + None => ctx.content_rect().center(), }); let swatches = d.str("tab") == "swatches"; let mut tab = None; - let resp = egui::Area::new(egui::Id::new("gradient-stop-popover")) + let resp = egui::Area::new(area) .order(egui::Order::Foreground) .fixed_pos(pos) .constrain(true) @@ -150,6 +157,34 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { } } +/// The popover's size before it has been drawn. +const POPOVER_SIZE: egui::Vec2 = vec2(272.0, 360.0); + +/// The selected art's box on the screen. +fn selection_on_screen(app: &VectorcraftApp, xf: &Xf) -> Option { + let st = app.session.active()?; + let b = st.doc.bounds_of(&st.selection.objects, false)?; + Some(egui::Rect::from_points(&xf.quad(b))) +} + +/// Where the popover opened from a stop's chip at `chip` goes: beside the selected `art` on the +/// first side of it with room for a popover of `size` in `room` (right, left, below, above), so +/// the art it recolours stays in view (#862); else beside the chip. +fn beside_art(chip: egui::Pos2, art: egui::Rect, size: egui::Vec2, room: egui::Rect) -> egui::Pos2 { + const GAP: f32 = 12.0; + let x = (chip.x - size.x / 2.0).clamp(room.left(), (room.right() - size.x).max(room.left())); + let y = (chip.y - size.y / 2.0).clamp(room.top(), (room.bottom() - size.y).max(room.top())); + [ + egui::pos2(art.right() + GAP, y), + egui::pos2(art.left() - GAP - size.x, y), + egui::pos2(x, art.bottom() + GAP), + egui::pos2(x, art.top() - GAP - size.y), + ] + .into_iter() + .find(|p| room.contains_rect(egui::Rect::from_min_size(*p, size))) + .unwrap_or(chip + vec2(14.0, 14.0)) +} + /// Document swatches as tiles: a click gives the stop (or point) that colour. fn swatch_grid(app: &mut VectorcraftApp, ui: &mut egui::Ui, target: &Target) { let colors = swatch_colors(app); @@ -248,6 +283,28 @@ mod tests { app } + /// #862: the popover goes beside the art, on the first side with room, not over it. + #[test] + fn the_popover_goes_beside_the_art_it_edits() { + let room = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(1200.0, 800.0)); + let size = egui::vec2(272.0, 360.0); + let place = |art: egui::Rect| egui::Rect::from_min_size(beside_art(art.center(), art, size, room), size); + // Room on the right. + let art = egui::Rect::from_min_max(egui::pos2(300.0, 300.0), egui::pos2(500.0, 450.0)); + assert!(place(art).left() > art.right() && room.contains_rect(place(art))); + // Against the right edge: on the left. + let art = egui::Rect::from_min_max(egui::pos2(900.0, 300.0), egui::pos2(1150.0, 450.0)); + assert!(place(art).right() < art.left()); + // As wide as the room: below it, or above it near the bottom. + let art = egui::Rect::from_min_max(egui::pos2(100.0, 20.0), egui::pos2(1100.0, 300.0)); + assert!(place(art).top() > art.bottom()); + let art = egui::Rect::from_min_max(egui::pos2(100.0, 420.0), egui::pos2(1100.0, 780.0)); + assert!(place(art).bottom() < art.top()); + // Nowhere to go: beside the chip, as before. + let art = room.shrink(10.0); + assert_eq!(beside_art(art.center(), art, size, room), art.center() + egui::vec2(14.0, 14.0)); + } + fn stops(app: &VectorcraftApp) -> Vec { match selected(app) { Some(Target::Stop(stops, _)) => stops,