From 59b9b90ee08563771469de55513240e0dc96e809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=9E=97=E6=A9=A6?= <108113065+yelintong@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:56:53 +0800 Subject: [PATCH 1/2] ui: show constraint guides for a selected layer in its frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split out of #932 (the 2 px outlines and missing frame radius handles there move the canvas away from the reference tool, but the constraint-guides part is good on its own — bflatastic, #issuecomment-6420566791). When a layer inside a non-auto-layout frame is selected, blue dashed guides show how it is pinned: a left/right/top/bottom constraint draws a line from the layer's edge midpoint to the frame edge, and both-axis constraints draw both. A centre constraint marks the layer itself with a short solid bar through its middle — horizontal, vertical, or a cross when centred on both axes — rather than a guide reaching the frame's centre. --- crates/ui-egui/src/canvas/overlays.rs | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/ui-egui/src/canvas/overlays.rs b/crates/ui-egui/src/canvas/overlays.rs index 675427c18..2bd58cd52 100644 --- a/crates/ui-egui/src/canvas/overlays.rs +++ b/crates/ui-egui/src/canvas/overlays.rs @@ -2,6 +2,7 @@ use super::builder::{builder_overlay, hatched, region_hatch}; use super::outline::{TextOnScreen, draw_outline, node_quad, outline_colour, quad, slot_badges, stroke, swap_highlight}; use super::rulers::{boards, fmt, rulers}; use super::*; +use figcraft_doc::{HConstraint, VConstraint}; /// Everything drawn over the rendered page; returns the frame labels drawn (frame, corners on the /// page) for the engine to hit-test. @@ -152,6 +153,11 @@ pub(super) fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec for id in &sel { draw_outline(app, painter, &doc, *id, stroke(col, 1.0)); } + // Constraint guides belong to the normal selection overlay: not while the Shape Builder + // reshapes by regions, nor while measuring to another layer (Alt), whose lines take over. + if app.session.tool.tool != ToolId::ShapeBuilder && app.session.tool.overlay.measure_box.is_none() { + constraint_lines(app, painter, &doc, &sel); + } // The shape builder reshapes by regions: no transform box. if app.session.tool.tool != ToolId::ShapeBuilder && let Some(b) = selection_box(&doc, &sel) @@ -366,6 +372,79 @@ fn transform_box( None } +/// Blue dashed guides showing how a selected layer is pinned within its parent frame, and a short +/// solid mark at the layer's centre when it is centred on an axis. A left constraint draws a +/// horizontal line from the layer's left-edge midpoint to the frame's left edge; right, top and +/// bottom likewise (the line is perpendicular to the edge it leaves). A centre constraint draws a +/// short bar through the layer's middle — a horizontal one, a vertical one, or both (a cross when +/// the layer is centred on both axes). Auto-layout frames ignore constraints, so no guides are +/// drawn there. +fn constraint_lines(app: &FigApp, painter: &egui::Painter, doc: &Document, sel: &[NodeId]) { + let s = stroke(accent(painter), 1.0); + let line = |a: Pos2, b: Pos2| { + painter.add(Shape::dashed_line(&[a, b], s, 4.0, 3.0)); + }; + // A short solid bar at the layer's centre: horizontal, vertical, or both (a cross). + let mark = |c: Pos2, half: f32, h: bool, v: bool| { + if h { + painter.line_segment([pos2(c.x - half, c.y), pos2(c.x + half, c.y)], s); + } + if v { + painter.line_segment([pos2(c.x, c.y - half), pos2(c.x, c.y + half)], s); + } + }; + for &id in sel { + let Some(n) = doc.node(id) else { continue }; + let Some(parent_id) = n.parent else { continue }; + let Some(parent) = doc.node(parent_id) else { continue }; + // Constraints bind a layer only to a non-auto-layout frame it sits inside. + if !parent.kind.is_frame_like() || parent.layout.is_some() { + continue; + } + let nw = doc.world_transform(id); + let pw = doc.world_transform(parent_id); + let (nw_w, nw_h) = (n.width, n.height); + let (pw_w, pw_h) = (parent.width, parent.height); + let sp = |p: Point| app.to_screen(p.x, p.y); + // The layer's edge midpoints and centre in page space. + let n_left = sp(nw * Point::new(0.0, nw_h / 2.0)); + let n_right = sp(nw * Point::new(nw_w, nw_h / 2.0)); + let n_top = sp(nw * Point::new(nw_w / 2.0, 0.0)); + let n_bottom = sp(nw * Point::new(nw_w / 2.0, nw_h)); + let n_c = sp(nw * Point::new(nw_w / 2.0, nw_h / 2.0)); + // The frame's edges and centre in page space (axis-aligned approximation). + let p_left = sp(pw * Point::new(0.0, 0.0)).x; + let p_right = sp(pw * Point::new(pw_w, 0.0)).x; + let p_top = sp(pw * Point::new(0.0, 0.0)).y; + let p_bottom = sp(pw * Point::new(0.0, pw_h)).y; + match n.constraints.horizontal { + HConstraint::Left => line(n_left, pos2(p_left, n_left.y)), + HConstraint::Right => line(n_right, pos2(p_right, n_right.y)), + HConstraint::LeftRight => { + line(n_left, pos2(p_left, n_left.y)); + line(n_right, pos2(p_right, n_right.y)); + } + HConstraint::Center | HConstraint::Scale => {} + } + match n.constraints.vertical { + VConstraint::Top => line(n_top, pos2(n_top.x, p_top)), + VConstraint::Bottom => line(n_bottom, pos2(n_bottom.x, p_bottom)), + VConstraint::TopBottom => { + line(n_top, pos2(n_top.x, p_top)); + line(n_bottom, pos2(n_bottom.x, p_bottom)); + } + VConstraint::Center | VConstraint::Scale => {} + } + // A centred axis marks the layer itself with a short bar through its middle (a cross when + // both axes are centred), rather than a guide reaching the frame's centre. + let hc = matches!(n.constraints.horizontal, HConstraint::Center); + let vc = matches!(n.constraints.vertical, VConstraint::Center); + if hc || vc { + mark(n_c, 8.0, hc, vc); + } + } +} + /// A canvas readout: white `text` on a `col` pill placed at `at` by `align` (its box returned). fn pill(painter: &egui::Painter, at: Pos2, align: Align2, text: String, col: Color32) -> Rect { let galley = painter.layout_no_wrap(text, semibold(11.0), Color32::WHITE); From 5f58acdb35afb46dec868ec7519f81ac8c0051dc Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 29 Oct 2026 09:17:25 +0700 Subject: [PATCH 2/2] #1406: guides for an absolute child of an auto layout frame, and a test --- crates/ui-egui/src/canvas/overlays.rs | 7 ++++--- crates/ui-egui/src/canvas_fixes_tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/canvas/overlays.rs b/crates/ui-egui/src/canvas/overlays.rs index 3d57659bf..15af030e0 100644 --- a/crates/ui-egui/src/canvas/overlays.rs +++ b/crates/ui-egui/src/canvas/overlays.rs @@ -419,7 +419,7 @@ fn transform_box( /// bottom likewise (the line is perpendicular to the edge it leaves). A centre constraint draws a /// short bar through the layer's middle — a horizontal one, a vertical one, or both (a cross when /// the layer is centred on both axes). Auto-layout frames ignore constraints, so no guides are -/// drawn there. +/// drawn there, except for a layer that ignores the auto layout (absolute position). fn constraint_lines(app: &FigApp, painter: &egui::Painter, doc: &Document, sel: &[NodeId]) { let s = stroke(accent(painter), 1.0); let line = |a: Pos2, b: Pos2| { @@ -438,8 +438,9 @@ fn constraint_lines(app: &FigApp, painter: &egui::Painter, doc: &Document, sel: let Some(n) = doc.node(id) else { continue }; let Some(parent_id) = n.parent else { continue }; let Some(parent) = doc.node(parent_id) else { continue }; - // Constraints bind a layer only to a non-auto-layout frame it sits inside. - if !parent.kind.is_frame_like() || parent.layout.is_some() { + // Constraints bind a layer only to a frame it sits inside: one without auto layout, or one + // whose auto layout the layer ignores (absolute position). + if !parent.kind.is_frame_like() || (parent.layout.is_some() && !n.child_layout.absolute) { continue; } let nw = doc.world_transform(id); diff --git a/crates/ui-egui/src/canvas_fixes_tests.rs b/crates/ui-egui/src/canvas_fixes_tests.rs index dc3f7e94c..59b2b0d6f 100644 --- a/crates/ui-egui/src/canvas_fixes_tests.rs +++ b/crates/ui-egui/src/canvas_fixes_tests.rs @@ -162,3 +162,27 @@ fn the_pencil_previews_its_stroke_in_its_colour_and_weight() { let w = preview(&flat(&u.frame_output(vec![egui::Event::PointerMoved(p)]).shapes)).expect("still previewing"); assert!((w - 1.0).abs() < 0.01, "at least a point: {w}"); } + +#[test] +fn a_selected_layer_shows_its_constraint_guides_in_its_frame() { + // A child pinned left (the default) in a frame without auto layout: a dashed guide runs from its + // left-edge midpoint to the frame's left edge. An auto layout frame places its children itself, + // so only a child that ignores the auto layout (absolute) shows one there. + let mut s = Session::new(); + let f = id(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 200, "height": 120})).unwrap()); + let c = id(&s.execute("node.create", &json!({"type": "rectangle", "x": 80, "y": 40, "width": 40, "height": 40, "parent": f})).unwrap()); + s.execute("select.set", &json!({"ids": [c]})).unwrap(); + let mut u = Ui::new(s); + let guide = |u: &mut Ui| { + let shapes = flat(&u.frame_output(vec![]).shapes); + let doc = u.app.session.doc().unwrap(); + let (fb, cb) = (doc.world_bounds(f).unwrap(), doc.world_bounds(c).unwrap()); + let y = cb.center().y; + dashes_along(&shapes, u.app.to_screen(fb.x0, y), u.app.to_screen(cb.x0, y)) + }; + assert!(guide(&mut u) > 0, "a left constraint draws a dashed guide to the frame's left edge"); + u.app.run("node.set", json!({"id": f, "props": {"layout": "horizontal", "padding": 40}})); + assert_eq!(guide(&mut u), 0, "auto layout places the child: no constraint guide"); + u.app.run("node.set", json!({"id": c, "props": {"absolute": true}})); + assert!(guide(&mut u) > 0, "an absolute child is pinned by its constraints again"); +}