mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-14 03:01:11 +00:00
Merge pull request #2298 from TrevorBrown-dev/fix/align-auto-layout-child
M8: Align is unavailable for a layer in an auto layout frame's flow, as in Figma
This commit is contained in:
@@ -336,3 +336,36 @@ fn auto_layout_keys_beside_no_auto_layout_are_refused() {
|
||||
set(&mut s, f, json!({"layout": null, "name": "Plain"}));
|
||||
assert!(s.doc().unwrap().node(f).unwrap().layout.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn align_is_unavailable_for_a_child_in_an_auto_layout_flow() {
|
||||
use crate::cmd::arrange::{align_blocked_by_auto_layout, can_align};
|
||||
let mut s = Session::new();
|
||||
let f = al_frame(&mut s, json!({"gap": 10}));
|
||||
let a = rect(&mut s, f, 20.0, 20.0, "#ff0000");
|
||||
let b = rect(&mut s, f, 20.0, 20.0, "#00ff00");
|
||||
let at = |s: &Session, i: NodeId| s.doc().unwrap().world_bounds(i).unwrap();
|
||||
let (before_a, before_b) = (at(&s, a), at(&s, b));
|
||||
assert_eq!(before_b.x0, 30.0, "the layout placed the second one after the first");
|
||||
let doc = s.doc().unwrap();
|
||||
assert!(!can_align(doc, &[b]), "Align is greyed out for one child of the frame");
|
||||
assert!(align_blocked_by_auto_layout(doc, &[b]), "because its frame places it");
|
||||
for cmd in ["align.left", "align.hcenter", "align.right", "align.top", "align.vcenter", "align.bottom"] {
|
||||
s.execute(cmd, &json!({"ids": [b]})).unwrap();
|
||||
assert_eq!((at(&s, a), at(&s, b)), (before_a, before_b), "{cmd} leaves the layout alone");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absolute_child_of_an_auto_layout_frame_still_aligns_to_it() {
|
||||
use crate::cmd::arrange::{align_blocked_by_auto_layout, can_align};
|
||||
let mut s = Session::new();
|
||||
let f = al_frame(&mut s, json!({}));
|
||||
let kid = rect(&mut s, f, 20.0, 20.0, "#ff0000");
|
||||
set(&mut s, kid, json!({"absolute": true}));
|
||||
let doc = s.doc().unwrap();
|
||||
assert!(can_align(doc, &[kid]));
|
||||
assert!(!align_blocked_by_auto_layout(doc, &[kid]));
|
||||
s.execute("align.right", &json!({"ids": [kid]})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().world_bounds(kid).unwrap().x1, 200.0);
|
||||
}
|
||||
|
||||
@@ -164,12 +164,27 @@ pub fn can_align(doc: &Document, ids: &[NodeId]) -> bool {
|
||||
align_target(doc, &roots(doc, ids)).is_some()
|
||||
}
|
||||
|
||||
/// Whether `ids` is one layer that its auto layout frame places: Figma greys out Align for it,
|
||||
/// since the frame decides where it goes (measured in Figma's web app, 2026-11-12).
|
||||
pub fn align_blocked_by_auto_layout(doc: &Document, ids: &[NodeId]) -> bool {
|
||||
matches!(roots(doc, ids).as_slice(), [id] if in_auto_layout_flow(doc, *id))
|
||||
}
|
||||
|
||||
/// Whether the layer sits in an auto layout frame and takes part in its flow (not absolutely positioned).
|
||||
fn in_auto_layout_flow(doc: &Document, id: NodeId) -> bool {
|
||||
let Some(n) = doc.node(id) else { return false };
|
||||
!n.child_layout.absolute && n.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.layout.is_some())
|
||||
}
|
||||
|
||||
/// What `roots` align to: one layer to its parent frame, several to their combined bounds.
|
||||
fn align_target(doc: &Document, roots: &[NodeId]) -> Option<Rect> {
|
||||
match roots {
|
||||
[] => None,
|
||||
[id] => {
|
||||
let parent = doc.node(*id)?.parent.filter(|pp| !doc.node(*pp).is_some_and(Node::is_page))?;
|
||||
if in_auto_layout_flow(doc, *id) {
|
||||
return None;
|
||||
}
|
||||
Some(doc.world_bounds(parent).unwrap_or_default())
|
||||
}
|
||||
_ => Some(doc.bounds_of(roots).unwrap_or_default()),
|
||||
|
||||
@@ -21,6 +21,7 @@ name = "English"
|
||||
"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "A collection of this file with these modes and variables: override their values per mode and add variables of its own"
|
||||
"A .figcraft file with this name already exists. Choose its full name to confirm replacement." = "A .figcraft file with this name already exists. Choose its full name to confirm replacement."
|
||||
"A later vote" = "A later vote"
|
||||
"A layer in an auto layout frame is placed by its frame" = "A layer in an auto layout frame is placed by its frame"
|
||||
"A layer on its own aligns to its frame: select more layers, or one in a frame" = "A layer on its own aligns to its frame: select more layers, or one in a frame"
|
||||
"A responsive product page: hero, features, figures, a quote and a footer" = "A responsive product page: hero, features, figures, a quote and a footer"
|
||||
"A spring plays until it settles: its mass, stiffness and damping set how long" = "A spring plays until it settles: its mass, stiffness and damping set how long"
|
||||
|
||||
@@ -237,3 +237,18 @@ fn the_gesture_start_stands_in_only_while_the_selection_is_only_moved() {
|
||||
app.session.ds_mut().unwrap().selection.pop();
|
||||
assert!(crate::selection_cache::moved(app.session.ds().unwrap()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_align_buttons_are_off_for_a_layer_in_an_auto_layout_flow() {
|
||||
let mut s = Session::new();
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 200, "height": 100})).unwrap());
|
||||
let kid = id(s.execute("shape.rectangle", &json!({"parent": f, "x": 0, "y": 0, "width": 20, "height": 20})).unwrap());
|
||||
s.execute("node.set", &json!({"id": f, "props": {"layout": "horizontal", "padding": 0, "gap": 0}})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [kid]})).unwrap();
|
||||
let app = FigApp::new(s, Box::new(NoServices));
|
||||
let ctx = Context::default();
|
||||
let ds = app.session.ds().unwrap();
|
||||
let pv = crate::right::position_values(&ctx, &ds.doc.clone(), &ds.selection.clone());
|
||||
assert!(!pv.can_align, "Align is greyed out");
|
||||
assert!(pv.align_blocked, "and the tip says the frame places the layer");
|
||||
}
|
||||
|
||||
@@ -614,6 +614,8 @@ fn position(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, shown: &Arc<Docu
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct PositionValues {
|
||||
pub can_align: bool,
|
||||
/// Align is off because the one selected layer is placed by its auto layout frame.
|
||||
pub align_blocked: bool,
|
||||
pub x: Option<f64>,
|
||||
pub y: Option<f64>,
|
||||
pub rot: Option<f64>,
|
||||
@@ -644,6 +646,7 @@ pub(crate) fn position_values(ctx: &egui::Context, doc: &Arc<Document>, sel: &[N
|
||||
let parent = |n: &Node| n.parent.and_then(|p| doc.node(p));
|
||||
crate::selection_cache::cached(ctx, "position", doc, sel, &[], || PositionValues {
|
||||
can_align: figcraft_engine::cmd::arrange::can_align(doc, sel),
|
||||
align_blocked: figcraft_engine::cmd::arrange::align_blocked_by_auto_layout(doc, sel),
|
||||
x: common(doc, sel, |n| x_of(doc, n)),
|
||||
y: common(doc, sel, |n| y_of(doc, n)),
|
||||
rot: common(doc, sel, |n| rot_of(doc, n)),
|
||||
@@ -662,7 +665,7 @@ pub(crate) fn position_values(ctx: &egui::Context, doc: &Arc<Document>, sel: &[N
|
||||
fn position_fields(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, sel: &[NodeId]) {
|
||||
let pv = position_values(ui.ctx(), doc, sel);
|
||||
let col = widgets::col_width(ui.available_width());
|
||||
align_row(app, ui, sel, pv.can_align, col);
|
||||
align_row(app, ui, sel, pv.can_align, pv.align_blocked, col);
|
||||
position_row(app, ui, doc, sel, &pv, col);
|
||||
rotation_row(app, ui, doc, sel, pv.rot, col);
|
||||
if pv.in_frame {
|
||||
@@ -671,7 +674,7 @@ fn position_fields(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, sel: &[No
|
||||
}
|
||||
|
||||
/// Alignment, and distribution and tidy up for three or more layers.
|
||||
fn align_row(app: &mut FigApp, ui: &mut Ui, sel: &[NodeId], can_align: bool, col: f32) {
|
||||
fn align_row(app: &mut FigApp, ui: &mut Ui, sel: &[NodeId], can_align: bool, blocked: bool, col: f32) {
|
||||
let ids = sel.to_vec();
|
||||
widgets::sublabels(ui, tr("Alignment"), None);
|
||||
row(ui, 32.0, |ui| {
|
||||
@@ -691,7 +694,12 @@ fn align_row(app: &mut FigApp, ui: &mut Ui, sel: &[NodeId], can_align: bool, col
|
||||
let v = widgets::segmented(ui, &items[3..], None, w).map(|i| i + 3);
|
||||
h.or(v)
|
||||
});
|
||||
picked.response.on_disabled_hover_text(tr("A layer on its own aligns to its frame: select more layers, or one in a frame"));
|
||||
let why = if blocked {
|
||||
tr("A layer in an auto layout frame is placed by its frame")
|
||||
} else {
|
||||
tr("A layer on its own aligns to its frame: select more layers, or one in a frame")
|
||||
};
|
||||
picked.response.on_disabled_hover_text(why);
|
||||
if let Some(i) = picked.inner {
|
||||
app.run(cmds[i], json!({"ids": ids}));
|
||||
}
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user