Merge pull request #9724 from penpot/alotor-fix-grid-position

🐛 Fix problem with grid child positions
This commit is contained in:
Alejandro Alonso
2026-05-19 16:57:39 +02:00
committed by GitHub

View File

@@ -651,6 +651,37 @@ pub fn grid_cell_data<'a>(
)
}
// Returns `(h_min, v_min, h_size, v_size)` — the child's bounding box expressed in the
// layout frame's own coordinate system (projected onto its `hv`/`vv` unit vectors).
//
// Using the frame axes rather than screen x/y is necessary when the parent grid frame
// is itself rotated: in that case `max_x - min_x` is the screen-AABB width, which
// differs from the width measured along the frame's horizontal axis.
fn child_frame_aabb(child_bounds: &Bounds, hv: Vector, vv: Vector) -> (f32, f32, f32, f32) {
let corners = child_bounds.points();
let mut h_min = f32::INFINITY;
let mut h_max = f32::NEG_INFINITY;
let mut v_min = f32::INFINITY;
let mut v_max = f32::NEG_INFINITY;
for p in &corners {
let h = hv.x * p.x + hv.y * p.y;
let v = vv.x * p.x + vv.y * p.y;
if h < h_min {
h_min = h;
}
if h > h_max {
h_max = h;
}
if v < v_min {
v_min = v;
}
if v > v_max {
v_max = v;
}
}
(h_min, v_min, h_max - h_min, v_max - v_min)
}
fn child_position(
child: &Shape,
layout_bounds: &Bounds,
@@ -667,12 +698,17 @@ fn child_position(
let margin_right = layout_item.map(|i| i.margin_right).unwrap_or(0.0);
let margin_bottom = layout_item.map(|i| i.margin_bottom).unwrap_or(0.0);
// Project corners onto the frame's own axes so that both a rotated child *and* a
// rotated parent frame are handled correctly. For an axis-aligned frame this
// reduces to max_x-min_x / max_y-min_y, so non-rotated layouts are unaffected.
let (_, _, child_width, child_height) = child_frame_aabb(child_bounds, hv, vv);
let vpos = match (cell.align_self, layout_data.align_items) {
(Some(AlignSelf::Start), _) => margin_top,
(Some(AlignSelf::Center), _) => (cell.height - child_bounds.height()) / 2.0,
(Some(AlignSelf::End), _) => margin_bottom + cell.height - child_bounds.height(),
(_, AlignItems::Center) => (cell.height - child_bounds.height()) / 2.0,
(_, AlignItems::End) => margin_bottom + cell.height - child_bounds.height(),
(Some(AlignSelf::Center), _) => (cell.height - child_height) / 2.0,
(Some(AlignSelf::End), _) => margin_bottom + cell.height - child_height,
(_, AlignItems::Center) => (cell.height - child_height) / 2.0,
(_, AlignItems::End) => margin_bottom + cell.height - child_height,
_ => margin_top,
};
@@ -684,10 +720,10 @@ fn child_position(
let hpos = match (cell.justify_self, layout_data.justify_items) {
(Some(JustifySelf::Start), _) => margin_left,
(Some(JustifySelf::Center), _) => (cell.width - child_bounds.width()) / 2.0,
(Some(JustifySelf::End), _) => margin_right + cell.width - child_bounds.width(),
(_, JustifyItems::Center) => (cell.width - child_bounds.width()) / 2.0,
(_, JustifyItems::End) => margin_right + cell.width - child_bounds.width(),
(Some(JustifySelf::Center), _) => (cell.width - child_width) / 2.0,
(Some(JustifySelf::End), _) => margin_right + cell.width - child_width,
(_, JustifyItems::Center) => (cell.width - child_width) / 2.0,
(_, JustifyItems::End) => margin_right + cell.width - child_width,
_ => margin_left,
};
@@ -747,11 +783,27 @@ pub fn reflow_grid_layout(
let Some(child) = cell.shape else { continue };
let child_bounds = bounds.find(child);
// Compute frame-axis projections once; used for both sizing and positioning.
let hv = layout_bounds.hv(1.0);
let vv = layout_bounds.vv(1.0);
let (h_min, v_min, child_frame_w, child_frame_h) = child_frame_aabb(&child_bounds, hv, vv);
// resize_matrix scales the child in the parent's local frame coordinate system
// by (new_width / child_bounds.width()) in the h-axis and
// (new_height / child_bounds.height()) in the v-axis. For a rotated child the
// frame-projected extent differs from the intrinsic bounds dimensions, so we
// back-calculate the intrinsic target that will produce the desired
// frame-projected extent.
let mut new_width = child_bounds.width();
if child.is_layout_horizontal_fill() {
let margin_left = child.layout_item.map(|i| i.margin_left).unwrap_or(0.0);
let margin_right = child.layout_item.map(|i| i.margin_right).unwrap_or(0.0);
new_width = cell.width - margin_left - margin_right;
let target_frame_w = cell.width - margin_left - margin_right;
new_width = if child_frame_w > MIN_SIZE {
target_frame_w * child_bounds.width() / child_frame_w
} else {
target_frame_w
};
let min_width = child.layout_item.and_then(|i| i.min_w).unwrap_or(MIN_SIZE);
let max_width = child.layout_item.and_then(|i| i.max_w).unwrap_or(MAX_SIZE);
new_width = new_width.clamp(min_width, max_width);
@@ -761,7 +813,12 @@ pub fn reflow_grid_layout(
if child.is_layout_vertical_fill() {
let margin_top = child.layout_item.map(|i| i.margin_top).unwrap_or(0.0);
let margin_bottom = child.layout_item.map(|i| i.margin_bottom).unwrap_or(0.0);
new_height = cell.height - margin_top - margin_bottom;
let target_frame_h = cell.height - margin_top - margin_bottom;
new_height = if child_frame_h > MIN_SIZE {
target_frame_h * child_bounds.height() / child_frame_h
} else {
target_frame_h
};
let min_height = child.layout_item.and_then(|i| i.min_h).unwrap_or(MIN_SIZE);
let max_height = child.layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE);
new_height = new_height.clamp(min_height, max_height);
@@ -792,7 +849,36 @@ pub fn reflow_grid_layout(
cell,
);
let delta_v = Vector::new_points(&child_bounds.nw, &position);
// Compute the child's reference point in the frame's coordinate system.
// For a rotated parent frame, (min_x, min_y) is wrong because it is the
// screen-AABB corner, not the frame-axis-aligned corner.
// child_ref = h_min * hv + v_min * vv gives the world-space point whose
// projections onto hv/vv are the child's minima along those axes —
// the "top-left in frame coordinates".
//
// For fill axes, resize_matrix scales local-x/y by (new_w / child_bounds.width())
// anchored at nw. This shifts h_min/v_min: the post-resize minimum is
// h_min_new = nw_h + (h_min - nw_h) * scale_w
// We must translate FROM this post-resize minimum, not the pre-resize one.
let nw_h = hv.x * child_bounds.nw.x + hv.y * child_bounds.nw.y;
let nw_v = vv.x * child_bounds.nw.x + vv.y * child_bounds.nw.y;
let h_anchor = if child.is_layout_horizontal_fill() && child_bounds.width() > MIN_SIZE {
let scale_w = new_width / child_bounds.width();
nw_h + (h_min - nw_h) * scale_w
} else {
h_min
};
let v_anchor = if child.is_layout_vertical_fill() && child_bounds.height() > MIN_SIZE {
let scale_h = new_height / child_bounds.height();
nw_v + (v_min - nw_v) * scale_h
} else {
v_min
};
let child_ref = Point::new(
h_anchor * hv.x + v_anchor * vv.x,
h_anchor * hv.y + v_anchor * vv.y,
);
let delta_v = Vector::new_points(&child_ref, &position);
if delta_v.x.abs() > MIN_SIZE || delta_v.y.abs() > MIN_SIZE {
transform.post_concat(&Matrix::translate(delta_v));