diff --git a/crates/op-pen-loader/src/adapter.rs b/crates/op-pen-loader/src/adapter.rs index fe8e00b42..6a673e06e 100644 --- a/crates/op-pen-loader/src/adapter.rs +++ b/crates/op-pen-loader/src/adapter.rs @@ -321,7 +321,22 @@ fn layout_measure_backend() -> Rc { /// root at its own (0, 0), so a tap must subtract the containing root's /// authored origin before it reaches `Runtime::dispatch_pointer`. pub fn root_authored_origin(n: &PenNode) -> (f32, f32) { - let base = match n { + let base = pen_base(n); + (base.x.unwrap_or(0.0) as f32, base.y.unwrap_or(0.0) as f32) +} + +/// `Some(_)` when the node authored an `x` or `y` — the explicit-absolute +/// signal `layout_repair`'s flow inference must yield to. +pub(crate) fn node_base_xy(n: &PenNode) -> Option<(f64, f64)> { + let base = pen_base(n); + match (base.x, base.y) { + (None, None) => None, + (x, y) => Some((x.unwrap_or(0.0), y.unwrap_or(0.0))), + } +} + +fn pen_base(n: &PenNode) -> &jian_ops_schema::node::base::PenNodeBase { + match n { PenNode::Frame(f) => &f.base, PenNode::Group(g) => &g.base, PenNode::Rectangle(r) => &r.base, @@ -343,8 +358,7 @@ pub fn root_authored_origin(n: &PenNode) -> (f32, f32) { PenNode::Image(i) => &i.base, PenNode::IconFont(i) => &i.base, PenNode::Ref(r) => &r.base, - }; - (base.x.unwrap_or(0.0) as f32, base.y.unwrap_or(0.0) as f32) + } } /// Choose the (available_width, available_height) the layout engine diff --git a/crates/op-pen-loader/src/layout_repair.rs b/crates/op-pen-loader/src/layout_repair.rs index ffaa727a0..879337b72 100644 --- a/crates/op-pen-loader/src/layout_repair.rs +++ b/crates/op-pen-loader/src/layout_repair.rs @@ -46,7 +46,9 @@ fn repair_node(node: &PenNode, rects: &mut BTreeMap, is_root: repair_horizontal_container(node, props, kids, rects); } None | Some(LayoutMode::None) - if is_frame(node) && should_infer_horizontal_layout(props, kids) => + if is_frame(node) + && should_infer_horizontal_layout(props, kids) + && !explicit_absolute_intent(props, kids) => { repair_inferred_horizontal_container(node, props, kids, rects); } @@ -437,6 +439,20 @@ fn should_infer_horizontal_layout(props: &ContainerProps, kids: &[PenNode]) -> b || kids.iter().any(child_has_fill_container_axis) } +/// An EXPLICITLY authored `layout: "none"` whose children carry real x/y is +/// absolute placement by spec — a badge dot pinned on a bell icon (x:28, y:8 +/// over a 20px icon at 12,12). Models belt-and-suspender these buttons with +/// justifyContent+alignItems too, which used to trip the flow inference and +/// stomp taffy's absolute positions (measured: the badge rendered BESIDE the +/// bell). Legacy TS docs with an ABSENT layout field and stale x/y=0 children +/// still get the inference — their coordinates are flow debris, not intent. +fn explicit_absolute_intent(props: &ContainerProps, kids: &[PenNode]) -> bool { + matches!(props.layout.as_ref(), Some(LayoutMode::None)) + && kids + .iter() + .any(|k| crate::adapter::node_base_xy(k).is_some_and(|(x, y)| x != 0.0 || y != 0.0)) +} + fn inferred_horizontal_padding(node: &PenNode, props: &ContainerProps, kids: &[PenNode]) -> Sides { let mut padding = padding_sides(props.padding.as_ref()); if legacy_zero_horizontal_inset_row(node, props, kids) {