From fd87ddd0b914f1a1f2fd6b607a96393fd25dae97 Mon Sep 17 00:00:00 2001
From: SGavrl <277843151+SGavrl@users.noreply.github.com>
Date: Tue, 4 Aug 2026 11:03:00 +0000
Subject: [PATCH] Transfer final flex sizes through aspect ratios
---
crates/obscura-render/tests/layout_test.rs | 117 ++++++++++++++++++
render-repros/checks.json | 6 +
render-repros/flex-post-ratio-cross-size.html | 53 ++++++++
vendor/taffy/src/compute/flexbox.rs | 107 +++++++++++++---
4 files changed, 265 insertions(+), 18 deletions(-)
create mode 100644 render-repros/flex-post-ratio-cross-size.html
diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs
index d5a0a54..3b2b386 100644
--- a/crates/obscura-render/tests/layout_test.rs
+++ b/crates/obscura-render/tests/layout_test.rs
@@ -309,6 +309,123 @@ fn percentage_padding_uses_final_flex_containing_block_width() {
}
}
+#[test]
+fn final_flex_main_size_drives_auto_cross_size_through_aspect_ratio() {
+ let tree = parse_html(include_str!("../../../render-repros/flex-post-ratio-cross-size.html"));
+ let layout = layout_dom(&tree, (1080.0, 1000.0));
+ let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
+
+ let row = rect("row");
+ let media = rect("media");
+ let visual = rect("visual");
+ let copy = rect("copy");
+ let after = rect("after");
+
+ assert!((media.width - 520.0).abs() < 0.01, "flexed media width: {media:?}");
+ assert!(
+ (media.height - 520.0 / 1.72).abs() < 0.51,
+ "auto cross-size must use the final 520px main size: {media:?}"
+ );
+ assert!(
+ (visual.width - 520.0).abs() < 0.01 && (visual.height - 520.0 / 1.75571).abs() < 0.51,
+ "absolute visual must use the ratio-sized containing block: {visual:?}"
+ );
+ assert!(
+ (copy.x - 560.0).abs() < 0.01 && (copy.width - 520.0).abs() < 0.01,
+ "the sibling must receive the other flexible share: {copy:?}"
+ );
+ assert!(
+ (row.height - media.height).abs() < 0.01 && (after.y - (row.height + 150.0)).abs() < 0.01,
+ "the ratio-sized item must contribute to its line and following flow: row={row:?} after={after:?}"
+ );
+}
+
+#[test]
+fn post_flex_aspect_transfer_respects_content_and_border_boxes() {
+ let tree = parse_html(
+ r#"
+
+
+
+
+
+ "#,
+ );
+ let layout = layout_dom(&tree, (500.0, 300.0));
+ let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
+ let content = rect("content");
+ let border = rect("border");
+ let row = rect("row");
+ let minimum = rect("minimum");
+ let maximum = rect("maximum");
+ let explicit = rect("explicit");
+
+ assert!(
+ (content.width - 200.0).abs() < 0.01 && (content.height - 115.0).abs() < 0.01,
+ "content-box ratio must exclude then restore 30px of padding and border: {content:?}"
+ );
+ assert!(
+ (border.width - 200.0).abs() < 0.01 && (border.height - 100.0).abs() < 0.01,
+ "authored border-box ratio must apply to the outer box: {border:?}"
+ );
+ assert!((row.height - 115.0).abs() < 0.01, "the tallest transferred cross-size sets the line: {row:?}");
+ assert!(
+ (minimum.width - 200.0).abs() < 0.01 && (minimum.height - 130.0).abs() < 0.01,
+ "the transferred cross-size must obey min-height: {minimum:?}"
+ );
+ assert!(
+ (maximum.width - 200.0).abs() < 0.01 && (maximum.height - 70.0).abs() < 0.01,
+ "the transferred cross-size must obey max-height: {maximum:?}"
+ );
+ assert!(
+ (explicit.width - 200.0).abs() < 0.01 && (explicit.height - 80.0).abs() < 0.01,
+ "an explicit cross-size must not be replaced by the preferred ratio: {explicit:?}"
+ );
+}
+
+#[cfg(feature = "paint")]
+#[test]
+fn scrolled_ratio_sized_flex_clip_keeps_absolute_visual_visible() {
+ let tree = parse_html(
+ r#"
+
+
+ "#,
+ );
+ let pixmap = obscura_render::paint_dom_scrolled(&tree, (1080.0, 400.0), None, (0.0, 8300.0))
+ .expect("scrolled ratio fixture paints");
+ let visual = pixmap.pixel(260, 100).expect("visual center pixel");
+ let clip_tail = pixmap.pixel(260, 299).expect("clip tail pixel");
+
+ assert!(
+ visual.red() == 47 && visual.green() == 158 && visual.blue() == 68,
+ "the absolute visual must survive its overflow clip: {visual:?}"
+ );
+ assert!(
+ clip_tail.red() == 112 && clip_tail.green() == 72 && clip_tail.blue() == 232,
+ "the ratio-sized clip owner must extend below the visual: {clip_tail:?}"
+ );
+}
+
#[test]
fn mixed_percentage_padding_shorthand_preserves_fixed_sides_and_content_origin() {
let tree = parse_html(
diff --git a/render-repros/checks.json b/render-repros/checks.json
index 9da45b6..dbbd113 100644
--- a/render-repros/checks.json
+++ b/render-repros/checks.json
@@ -83,6 +83,12 @@
{"name": "invalid shorthand does not partially mutate", "color": "#d6336c", "x": 200, "y": 160, "width": 100, "height": 40},
{"name": "CSS supports accepts only the shorthand grammar", "color": "#18723a", "x": 400, "y": 120, "width": 180, "height": 40}
],
+ "flex-post-ratio-cross-size": [
+ {"name": "absolute visual survives the ratio-sized overflow clip", "color": "#2f9e44", "x": 0, "y": 0, "width": 520, "height": 296},
+ {"name": "final flex width sizes the media cross axis", "color": "#7048e8", "x": 0, "y": 296, "width": 520, "height": 6},
+ {"name": "sibling receives the other flexible share", "color": "#1971c2", "x": 560, "y": 0, "width": 340, "height": 150},
+ {"name": "following content starts after ratio height and column gap", "color": "#e8590c", "x": 0, "y": 452, "width": 900, "height": 20}
+ ],
"flex-whitespace-items": [
{"name": "direct flex sidebar starts at container edge", "color": "#1971c2", "x": 0, "y": 0, "width": 225, "height": 180},
{"name": "direct flex body keeps its intended inset", "color": "#2b8a3e", "x": 225, "y": 0, "width": 675, "height": 200},
diff --git a/render-repros/flex-post-ratio-cross-size.html b/render-repros/flex-post-ratio-cross-size.html
new file mode 100644
index 0000000..0520faf
--- /dev/null
+++ b/render-repros/flex-post-ratio-cross-size.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/taffy/src/compute/flexbox.rs b/vendor/taffy/src/compute/flexbox.rs
index c526323..f72c980 100644
--- a/vendor/taffy/src/compute/flexbox.rs
+++ b/vendor/taffy/src/compute/flexbox.rs
@@ -24,6 +24,31 @@ fn resolve_flex_normal(alignment: AlignSelf) -> AlignSelf {
if alignment.keyword == AlignItemsKeyword::Normal { AlignSelf::STRETCH } else { alignment }
}
+/// Transfer a definite border-box size through a preferred aspect ratio while
+/// applying the ratio to the box selected by CSS `box-sizing` and intrinsic
+/// ratio provenance.
+#[inline]
+fn maybe_apply_preferred_aspect_ratio(
+ size: Size