"#,
+ );
+ let laid = layout_dom(&tree, (400.0, 200.0));
+ let inherited = tree.get_element_by_id("inherited").unwrap();
+ let normal = tree.get_element_by_id("normal").unwrap();
+
+ assert_eq!(
+ laid.styles[&inherited].white_space,
+ Some(crate::WhiteSpace::Pre),
+ "a block descendant must inherit the preformatted wrapping mode"
+ );
+ assert_eq!(
+ laid.styles[&normal].white_space,
+ Some(crate::WhiteSpace::Normal),
+ "an explicit descendant value must override inherited pre"
+ );
+ assert_eq!(
+ laid.styles[&inherited]
+ .before_pseudo
+ .as_ref()
+ .unwrap()
+ .white_space,
+ Some(crate::WhiteSpace::Pre),
+ "a pseudo-element must inherit the originating element's computed wrapping mode"
+ );
+ assert_eq!(
+ laid.rects[&inherited].height, 20.0,
+ "the inherited pre line must overflow horizontally without wrapping"
+ );
+ assert!(
+ laid.rects[&normal].height > 20.0,
+ "the explicit normal line should still wrap at the containing width"
+ );
+ }
+
+ #[test]
+ fn generated_inline_boxes_apply_sizes_only_after_display_blockification() {
+ let tree = parse_html(
+ r#"
+
+
+ "#,
+ );
+ let laid = layout_dom(&tree, (500.0, 300.0));
+ let flex = tree.get_element_by_id("flex").unwrap();
+ let float = tree.get_element_by_id("float").unwrap();
+ let relative = tree.get_element_by_id("relative").unwrap();
+
+ let pseudo = |host| {
+ laid.generated_boxes
+ .iter()
+ .find(|generated| {
+ generated.host == host && generated.kind == GeneratedBoxKind::Before
+ })
+ .unwrap()
+ .rect
+ };
+ assert_eq!(
+ laid.styles[&flex].before_pseudo.as_ref().unwrap().display,
+ crate::Display::Block,
+ "a generated flex item must be blockified"
+ );
+ assert_eq!(
+ laid.styles[&float].before_pseudo.as_ref().unwrap().display,
+ crate::Display::Block,
+ "a floated generated box must be blockified"
+ );
+ assert_eq!(
+ laid.styles[&relative]
+ .before_pseudo
+ .as_ref()
+ .unwrap()
+ .display,
+ crate::Display::Inline,
+ "relative positioning does not make an ordinary inline atomic"
+ );
+ assert_eq!(pseudo(flex).width, 250.0);
+ assert_eq!(pseudo(flex).height, 120.0);
+ assert_eq!(pseudo(float).width, 80.0);
+ assert_eq!(pseudo(float).height, 40.0);
+ assert!(
+ pseudo(relative).width < 80.0 && pseudo(relative).height < 40.0,
+ "an ordinary inline pseudo must ignore authored box sizes"
+ );
+ }
+
#[test]
fn positioned_pseudo_inherits_the_hosts_computed_font_metrics() {
let tree = parse_html(
diff --git a/crates/obscura-render/src/inline.rs b/crates/obscura-render/src/inline.rs
index 33cc536..7362679 100644
--- a/crates/obscura-render/src/inline.rs
+++ b/crates/obscura-render/src/inline.rs
@@ -1812,7 +1812,7 @@ fn is_pure_text_ifc(
/// contains one is never a pure-text IFC (it keeps the general build path,
/// where the element gets a real taffy box). This is by tag, not display, so a
/// stylesheet setting `img{display:inline}` cannot trick us into folding it.
-fn is_replaced(local: &str) -> bool {
+pub(crate) fn is_replaced(local: &str) -> bool {
matches!(
local,
"img" | "svg" | "canvas" | "video" | "audio" | "iframe" | "embed"
diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs
index 47de304..e05109d 100644
--- a/crates/obscura-render/src/lib.rs
+++ b/crates/obscura-render/src/lib.rs
@@ -512,6 +512,14 @@ pub struct LayoutStyle {
/// (`` or its selected ``), rather than authored
/// CSS. A decoded image's natural ratio replaces this provisional ratio.
pub aspect_ratio_is_mapped: bool,
+ /// Whether this element generates a replaced box.
+ ///
+ /// Replaced elements with `display:inline` are atomic and therefore keep
+ /// authored width/height/min/max sizing. Ordinary inline boxes compute
+ /// those properties but ignore them for used layout. This provenance
+ /// cannot be inferred from intrinsic dimensions because an unloaded or
+ /// broken image remains replaced.
+ pub(crate) is_replaced_box: bool,
pub margin: Edges,
/// Which margin sides are `auto` (top, right, bottom, left). `margin: 0
/// auto` / `margin-inline: auto` centering needs a real Auto margin, which
@@ -994,6 +1002,13 @@ pub(crate) const CB_TRIGGER_CONTENT_VISIBILITY: u16 = 1 << 6;
pub(crate) const CB_TRIGGER_TRANSLATE: u16 = 1 << 7;
impl LayoutStyle {
+ /// Whether CSS box sizes compute normally but do not apply to this box's
+ /// used geometry. The display value here is post-blockification, so roots,
+ /// flex/grid items, floats, and out-of-flow boxes retain applicable sizes.
+ pub(crate) fn ignores_used_box_sizes(&self) -> bool {
+ self.display == Display::Inline && !self.is_inline_block && !self.is_replaced_box
+ }
+
pub(crate) fn establishes_positioning_containing_block(&self) -> bool {
self.containing_block_triggers != 0
}
@@ -1347,6 +1362,21 @@ pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style {
s.aspect_ratio = Some(ar);
}
}
+ if style.ignores_used_box_sizes() {
+ s.size = taffy::Size {
+ width: taffy::Dimension::auto(),
+ height: taffy::Dimension::auto(),
+ };
+ s.min_size = taffy::Size {
+ width: taffy::Dimension::auto(),
+ height: taffy::Dimension::auto(),
+ };
+ s.max_size = taffy::Size {
+ width: taffy::Dimension::auto(),
+ height: taffy::Dimension::auto(),
+ };
+ s.aspect_ratio = None;
+ }
if style.display != Display::Block {
if let Some(ai) = style.align_items {
s.align_items = Some(ai);
diff --git a/crates/obscura-render/src/paint.rs b/crates/obscura-render/src/paint.rs
index fd1ba33..91ac45a 100644
--- a/crates/obscura-render/src/paint.rs
+++ b/crates/obscura-render/src/paint.rs
@@ -391,6 +391,9 @@ impl PreparedRender {
) -> Option<(f32, f32)> {
let rect = self.layout.rects.get(&id)?;
let style = self.layout.styles.get(&id)?;
+ if style.ignores_used_box_sizes() {
+ return Some((0.0, 0.0));
+ }
Some((
(rect.width - style.border.left - style.border.right).max(0.0),
(rect.height - style.border.top - style.border.bottom).max(0.0),
@@ -482,7 +485,10 @@ impl PreparedRender {
},
);
- if let Some(rect) = rect {
+ if style.ignores_used_box_sizes() {
+ out.insert("width", dimension_css(style.width, "auto"));
+ out.insert("height", dimension_css(style.height, "auto"));
+ } else if let Some(rect) = rect {
let horizontal_non_content =
style.border.left + style.border.right + style.padding.left + style.padding.right;
let vertical_non_content =
diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs
index 282e88f..e2c3c1e 100644
--- a/crates/obscura-render/tests/layout_test.rs
+++ b/crates/obscura-render/tests/layout_test.rs
@@ -1272,6 +1272,49 @@ fn text_alignment_does_not_shrink_block_children_or_wrap_inline_block_rows() {
);
}
+#[test]
+fn non_replaced_inline_ignores_used_width_height_and_min_max_sizes() {
+ let tree = parse_html(
+ r#"
+
A tokenafter
"#,
+ );
+ let laid = layout_dom(&tree, (400.0, 200.0));
+ let token_id = tree.get_element_by_id("token").unwrap();
+ let after_id = tree.get_element_by_id("after").unwrap();
+ let token = laid.rects[&token_id];
+ let after = laid.rects[&after_id];
+
+ assert_eq!(
+ laid.styles[&token_id].width,
+ obscura_render::Dimension::Percent(1.0),
+ "computed style must retain the authored percentage"
+ );
+ assert!(
+ token.width > 20.0 && token.width < 100.0,
+ "a non-replaced inline's used width must hug its text and padding: {token:?}"
+ );
+ assert!(
+ token.height < 40.0,
+ "height/min-height/max-height do not apply to a non-replaced inline: {token:?}"
+ );
+ assert!(
+ after.x >= token.x + token.width - 0.01 && (after.y - token.y).abs() < 0.01,
+ "the following inline must remain on the same line: {token:?} {after:?}"
+ );
+}
+
#[test]
fn inline_svg_derives_auto_height_from_view_box() {
let tree = parse_html(