Support RTL layout and video posters

This commit is contained in:
SGavrl committed 2026-08-04 09:04:45 +00:00
1 parent 68c81f319c
commit 012a7dce2f
9 files changed
+473 -77

No files matched your search

+88 -20
View File
@@ -1916,6 +1916,13 @@ pub(crate) fn resolve_translate(d: crate::Dimension, basis: f32) -> f32 {
/// Apply HTML presentational attributes at their cascade origin: above the UA
/// defaults, but below every author stylesheet and style attribute.
fn apply_presentational_hints(node: &obscura_dom::tree::Node, style: &mut crate::LayoutStyle) {
if let Some(direction) = node.get_attribute("dir") {
style.direction = match direction.trim().to_ascii_lowercase().as_str() {
"ltr" => Some(taffy::Direction::Ltr),
"rtl" => Some(taffy::Direction::Rtl),
_ => style.direction,
};
}
if let Some(color) = node.get_attribute("color") {
crate::style::apply_inline(style, &format!("color: {}", color));
}
@@ -3912,6 +3919,7 @@ fn layout_dom_once(
#[derive(Clone)]
struct Inherited {
display: crate::Display,
direction: taffy::Direction,
display_contents: bool,
is_inline_block: bool,
flow_root: bool,
@@ -3963,6 +3971,7 @@ fn layout_dom_once(
// CSS Display's initial outer/inner value is inline flow.
// The root is blockified after computed values settle.
display: crate::Display::Inline,
direction: taffy::Direction::Ltr,
display_contents: false,
is_inline_block: false,
flow_root: false,
@@ -4063,6 +4072,7 @@ fn layout_dom_once(
vh,
);
inh.display = style.display;
inh.direction = style.direction.unwrap_or(inh.direction);
inh.display_contents = style.display_contents;
inh.is_inline_block = style.is_inline_block;
inh.flow_root = style.flow_root;
@@ -4245,6 +4255,10 @@ fn layout_dom_once(
style.display_inherit = false;
}
inh.display = style.display;
match style.direction {
Some(direction) => inh.direction = direction,
None => style.direction = Some(inh.direction),
}
inh.display_contents = style.display_contents;
inh.is_inline_block = style.is_inline_block;
inh.flow_root = style.flow_root;
@@ -4678,6 +4692,7 @@ fn layout_dom_once(
let host_text_indent = style.text_indent;
let host_invisible = style.effectively_invisible;
let host_display = style.display;
let host_direction = style.direction.unwrap_or(taffy::Direction::Ltr);
let host_display_contents = style.display_contents;
let host_is_inline_block = style.is_inline_block;
let host_flow_root = style.flow_root;
@@ -4702,6 +4717,9 @@ fn layout_dom_once(
0
};
let settle_pseudo = |pseudo: &mut crate::LayoutStyle| {
if pseudo.direction.is_none() {
pseudo.direction = Some(host_direction);
}
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();
@@ -6986,10 +7004,9 @@ fn fixed_grid_tracks(widths: &[f32]) -> Vec<taffy::GridTemplateComponent<String>
/// growth limit fits, `justify-content:normal` stretches all auto tracks by an
/// equal share; narrower/cyclic cases decline this fast path.
///
/// The current style model does not expose orthogonal writing modes and emits
/// Taffy's default LTR direction for every grid; this physical-column reduction
/// therefore cannot accidentally enter an RTL/orthogonal layout path that the
/// surrounding renderer does not implement yet. Percentage-width parents are
/// The current style model does not expose orthogonal writing modes. RTL grids
/// are deliberately excluded because this physical-column reduction has not
/// yet been mirrored into logical track coordinates. Percentage-width parents are
/// accepted only after the preliminary layout produced finite, explicit used
/// track sizes; that resolved track sum is the definite basis for pass two.
fn apply_full_span_column_subgrids<F>(
@@ -7008,6 +7025,7 @@ where
for (&dom, style) in styles {
if style.display != crate::Display::Grid
|| style.direction == Some(taffy::Direction::Rtl)
|| style.grid_template_columns_subgrid
|| !matches!(
style.width,
@@ -8452,6 +8470,10 @@ fn build_any(
}
}
let outer_style = taffy::Style {
direction: styles
.get(&id)
.and_then(|style| style.direction)
.unwrap_or(taffy::Direction::Ltr),
display: taffy::style::Display::Flex,
flex_direction: taffy::FlexDirection::Row,
flex_wrap: taffy::FlexWrap::Wrap,
@@ -10869,28 +10891,33 @@ fn build(
}
}
if let Some((width, height)) = crate::inline::default_replaced_intrinsic_size(
_name.local.as_ref(),
style.font_size.unwrap_or(16.0),
node.get_attribute("controls").is_some(),
_name.local.as_ref() != "embed"
|| node
.get_attribute("src")
.is_some_and(|value| !value.trim().is_empty()),
) {
let context = engine.register_replaced(width, height, style);
let leaf = taffy_tree
.new_leaf_with_context(taffy_style, context)
.ok()?;
id_map.insert(leaf, id);
return Some(leaf);
// A loaded poster supplies `<video>`'s replaced-content dimensions before
// decoded video metadata exists. Only use HTML's 300x150 fallback when no
// poster intrinsic is available.
if !(_name.local.as_ref() == "video" && style.replaced_intrinsic.is_some()) {
if let Some((width, height)) = crate::inline::default_replaced_intrinsic_size(
_name.local.as_ref(),
style.font_size.unwrap_or(16.0),
node.get_attribute("controls").is_some(),
_name.local.as_ref() != "embed"
|| node
.get_attribute("src")
.is_some_and(|value| !value.trim().is_empty()),
) {
let context = engine.register_replaced(width, height, style);
let leaf = taffy_tree
.new_leaf_with_context(taffy_style, context)
.ok()?;
id_map.insert(leaf, id);
return Some(leaf);
}
}
// A replaced image is a measured leaf, even when CSS gives it a percentage
// width. Its intrinsic dimensions participate in an auto-sized ancestor's
// max-content measurement; once the percentage axis becomes definite, the
// measure callback derives the other axis through the intrinsic ratio.
if _name.local.as_ref() == "img" {
if matches!(_name.local.as_ref(), "img" | "video") {
if let Some(intrinsic) = style.replaced_intrinsic.or_else(|| {
style
.intrinsic_size
@@ -11955,6 +11982,7 @@ fn run_wrapper_style(parent: &crate::LayoutStyle, has_text_strut: bool) -> taffy
0.0
};
taffy::Style {
direction: parent.direction.unwrap_or(taffy::Direction::Ltr),
display: taffy::style::Display::Flex,
flex_direction: taffy::FlexDirection::Row,
flex_wrap: taffy::FlexWrap::Wrap,
@@ -16588,6 +16616,46 @@ mod tests {
);
}
#[test]
fn inherited_rtl_direction_controls_flex_grid_and_block_start_edges() {
let tree = parse_html(
r#"<style>
html,body { margin:0; direction:rtl }
.case { width:300px; height:40px; margin-bottom:20px }
.flex { display:flex }
.grid { display:grid; grid-template-columns:50px 50px }
.item { width:50px; height:40px }
.block > div { width:50px; height:40px }
</style>
<div id="flex" class="case flex"><div id="flex-one" class="item"></div><div id="flex-two" class="item"></div></div>
<div id="grid" class="case grid"><div id="grid-one" class="item"></div><div id="grid-two" class="item"></div></div>
<div class="case block"><div id="block"></div></div>"#,
);
let laid = layout_dom(&tree, (900.0, 300.0));
let x = |id: &str| laid.rects[&tree.get_element_by_id(id).unwrap()].x;
assert!((x("flex-one") - 850.0).abs() < 0.1, "{}", x("flex-one"));
assert!((x("flex-two") - 800.0).abs() < 0.1, "{}", x("flex-two"));
assert!((x("grid-one") - 850.0).abs() < 0.1, "{}", x("grid-one"));
assert!((x("grid-two") - 800.0).abs() < 0.1, "{}", x("grid-two"));
assert!((x("block") - 850.0).abs() < 0.1, "{}", x("block"));
for id in ["flex", "grid"] {
let node = tree.get_element_by_id(id).unwrap();
assert_eq!(laid.styles[&node].direction, Some(taffy::Direction::Rtl));
}
}
#[test]
fn html_dir_hint_is_overridden_by_author_css_and_inherits() {
let tree = parse_html(
r#"<style>#override { direction:ltr }</style>
<div dir="rtl"><div id="inherited"></div><div id="override"></div></div>"#,
);
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("inherited").direction, Some(taffy::Direction::Rtl));
assert_eq!(style("override").direction, Some(taffy::Direction::Ltr));
}
#[test]
fn authored_table_with_mixed_direct_content_preserves_non_cell_child() {
let tree = parse_html(