From d6a575a62e99cf96e557ffb5c2a02b05a758a75f Mon Sep 17 00:00:00 2001
From: SGavrl <277843151+SGavrl@users.noreply.github.com>
Date: Tue, 4 Aug 2026 09:30:31 +0000
Subject: [PATCH] Implement logical border cascade geometry
---
crates/obscura-render/src/dom.rs | 29 ++
crates/obscura-render/src/lib.rs | 31 ++
crates/obscura-render/src/style.rs | 604 +++++++++++++++++++++++++++--
render-repros/checks.json | 8 +
render-repros/logical-borders.html | 12 +
5 files changed, 642 insertions(+), 42 deletions(-)
create mode 100644 render-repros/logical-borders.html
diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs
index 624e383..cf847ab 100644
--- a/crates/obscura-render/src/dom.rs
+++ b/crates/obscura-render/src/dom.rs
@@ -4259,6 +4259,7 @@ fn layout_dom_once(
Some(direction) => inh.direction = direction,
None => style.direction = Some(inh.direction),
}
+ crate::style::resolve_logical_borders(style);
inh.display_contents = style.display_contents;
inh.is_inline_block = style.is_inline_block;
inh.flow_root = style.flow_root;
@@ -4720,6 +4721,7 @@ fn layout_dom_once(
if pseudo.direction.is_none() {
pseudo.direction = Some(host_direction);
}
+ crate::style::resolve_logical_borders(pseudo);
if pseudo.grid_auto_columns_inherit {
pseudo.grid_auto_columns = host_grid_auto_columns.clone();
pseudo.grid_calc_expressions[2] = host_grid_auto_column_calcs.clone();
@@ -16656,6 +16658,33 @@ mod tests {
assert_eq!(style("override").direction, Some(taffy::Direction::Ltr));
}
+ #[test]
+ fn inherited_direction_resolves_logical_border_cascade_after_stylesheets() {
+ let tree = parse_html(
+ r#"
+
+ "#,
+ );
+ let laid = layout_dom(&tree, (900.0, 300.0));
+ let style = |id: &str| &laid.styles[&tree.get_element_by_id(id).unwrap()];
+ assert_eq!(style("logical-last").border.right, 12.0);
+ // ID specificity wins regardless of source order: the physical alias
+ // and logical property compete for the same final RTL side.
+ assert_eq!(
+ style("logical-last").border_model.colors.right,
+ Some([128, 0, 128, 255])
+ );
+ assert_eq!(style("physical-last").border.right, 4.0);
+ assert_eq!(style("physical-last").border_model.colors.right, Some([0, 128, 128, 255]));
+ }
+
#[test]
fn authored_table_with_mixed_direct_content_preserves_non_cell_child() {
let tree = parse_html(
diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs
index fde670c..1155c3a 100644
--- a/crates/obscura-render/src/lib.rs
+++ b/crates/obscura-render/src/lib.rs
@@ -915,6 +915,12 @@ pub struct LayoutStyle {
/// consumed by layout; it is zero on `none`/`hidden` sides while this
/// model retains their specified widths for later cascade declarations.
pub border_model: BorderModel,
+ /// Border state before the first cascaded physical/logical border
+ /// declaration. Logical sides cannot be mapped until inherited direction
+ /// is known, so the top-down pass replays `border_cascade_ops` over this
+ /// snapshot in exact cascade order.
+ pub(crate) border_cascade_base: Option,
+ pub(crate) border_cascade_ops: Vec,
/// Outline paint state. It deliberately has no counterpart in Taffy:
/// outlines never contribute to box geometry.
pub outline: OutlineModel,
@@ -1384,6 +1390,31 @@ pub struct LayoutStyle {
pub box_shadow: Option,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum BorderCascadeSide {
+ Top,
+ Right,
+ Bottom,
+ Left,
+ Inline,
+ InlineStart,
+ InlineEnd,
+ Block,
+ BlockStart,
+ BlockEnd,
+ All,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct BorderCascadeOp {
+ pub side: BorderCascadeSide,
+ pub width: Option,
+ pub style: Option,
+ /// Outer `None` means this operation leaves color unchanged; inner `None`
+ /// is the valid computed `currentcolor` value.
+ pub color: Option