diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index df6a562..b1af896 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -1242,6 +1242,7 @@ fn apply_presentational_hints(node: &obscura_dom::tree::Node, style: &mut crate: if w > 0.0 && h > 0.0 { style.aspect_ratio = Some(w / h); style.aspect_ratio_is_mapped = true; + style.aspect_ratio_is_intrinsic = true; } } } @@ -1265,6 +1266,7 @@ fn apply_presentational_hints(node: &obscura_dom::tree::Node, style: &mut crate: .collect(); if values.len() == 4 && values[2] > 0.0 && values[3] > 0.0 { style.aspect_ratio = Some(values[2] / values[3]); + style.aspect_ratio_is_intrinsic = true; } } } @@ -1355,6 +1357,7 @@ fn apply_picture_source_hints( _ => None, }; style.aspect_ratio_is_mapped = style.aspect_ratio.is_some(); + style.aspect_ratio_is_intrinsic = style.aspect_ratio.is_some(); } /// Compute the UA + author style for every element in preorder, maintaining @@ -1394,6 +1397,11 @@ fn cascade_walk( } let mut style = crate::style::ua_style(elem.local.as_ref()); style.is_replaced_box = crate::inline::is_replaced(elem.local.as_ref()); + style.has_replaced_sizing = crate::inline::has_replaced_sizing(elem.local.as_ref()) + || (elem.local.as_ref() == "input" + && node + .get_attribute("type") + .is_some_and(|value| value.eq_ignore_ascii_case("image"))); style.color_scheme_dark = inherited_color_scheme_dark; if matches!(elem.local.as_ref(), "dir" | "dl" | "menu" | "ol" | "ul") { let mut list_ancestor_count = 0usize; @@ -1527,6 +1535,7 @@ fn cascade_walk( } custom_properties.insert(id, this_props.clone()); style.is_replaced_box |= style.content_image.is_some(); + style.has_replaced_sizing |= style.content_image.is_some(); let (mut before_pseudo, mut after_pseudo) = if let Some(evaluator) = container_evaluator.as_deref_mut() { sheet.pseudo_styles_with_container_queries( @@ -1545,6 +1554,7 @@ fn cascade_walk( .flatten() { pseudo.is_replaced_box = pseudo.content_image.is_some(); + pseudo.has_replaced_sizing = pseudo.content_image.is_some(); } style.before_content = before_pseudo .as_ref() @@ -3030,7 +3040,11 @@ fn layout_dom_once( .filter_map(|(&id, style)| { let node = tree.get_node(id)?; let element = node.as_element()?; - (element.local.as_ref() == "button" && style.width == crate::Dimension::Auto).then( + (element.local.as_ref() == "button" + && style.width == crate::Dimension::Auto + && container_auto_inline_size(tree, id, style, &styles) + != ContainerAutoInlineSize::StretchedGridItem) + .then( || { let font_size = style.font_size.unwrap_or(13.333_333).max(1.0); ( @@ -3041,6 +3055,24 @@ fn layout_dom_once( ) }) .collect(); + let native_control_grid_stretch: HashMap = styles + .iter() + .filter_map(|(&id, style)| { + let node = tree.get_node(id)?; + let element = node.as_element()?; + matches!(element.local.as_ref(), "input" | "select").then(|| { + ( + id, + ( + container_auto_inline_size(tree, id, style, &styles) + == ContainerAutoInlineSize::StretchedGridItem, + container_auto_block_size(tree, id, style, &styles) + == ContainerAutoBlockSize::StretchedGridItem, + ), + ) + }) + }) + .collect(); for (&id, style) in styles.iter_mut() { let Some(node) = tree.get_node(id) else { continue; @@ -3048,7 +3080,10 @@ fn layout_dom_once( let Some(element) = node.as_element() else { continue; }; - if element.local.as_ref() == "button" && style.width == crate::Dimension::Auto { + if element.local.as_ref() == "button" + && style.width == crate::Dimension::Auto + && native_button_contents.contains_key(&id) + { // Buttons remain intrinsically sized form controls even when // author CSS changes their inner display to flex/grid. Treating // `display:flex` as an ordinary block makes an auto-width @@ -3160,7 +3195,9 @@ fn layout_dom_once( let intrinsic_height = crate::inline::used_line_height(style).max(1.0) * rows + vertical_edges; - if style.width == crate::Dimension::Auto { + let (stretch_inline, stretch_block) = + native_control_grid_stretch.get(&id).copied().unwrap_or_default(); + if style.width == crate::Dimension::Auto && !stretch_inline { style.width = crate::Dimension::Px( if style.box_sizing == crate::BoxSizing::ContentBox { label_width @@ -3169,7 +3206,7 @@ fn layout_dom_once( }, ); } - if style.height == crate::Dimension::Auto { + if style.height == crate::Dimension::Auto && !stretch_block { style.height = crate::Dimension::Px( if style.box_sizing == crate::BoxSizing::ContentBox { (intrinsic_height - vertical_edges).max(0.0) @@ -3194,6 +3231,8 @@ fn layout_dom_once( } let font_size = style.font_size.unwrap_or(13.333_333).max(1.0); + let (stretch_inline, stretch_block) = + native_control_grid_stretch.get(&id).copied().unwrap_or_default(); let horizontal_edges = style.padding.left + style.padding.right + style.border.left @@ -3240,7 +3279,7 @@ fn layout_dom_once( ) } }; - if style.width == crate::Dimension::Auto { + if style.width == crate::Dimension::Auto && !stretch_inline { let declared_width = if style.box_sizing == crate::BoxSizing::ContentBox { (intrinsic_width - horizontal_edges).max(0.0) } else { @@ -3248,7 +3287,7 @@ fn layout_dom_once( }; style.width = crate::Dimension::Px(declared_width); } - if style.height == crate::Dimension::Auto { + if style.height == crate::Dimension::Auto && !stretch_block { let declared_height = if style.box_sizing == crate::BoxSizing::ContentBox { (intrinsic_height - vertical_edges).max(0.0) } else { @@ -3350,6 +3389,7 @@ fn layout_dom_once( if s.aspect_ratio.is_none() || s.aspect_ratio_is_mapped { s.aspect_ratio = Some(iw / ih); s.aspect_ratio_is_mapped = false; + s.aspect_ratio_is_intrinsic = true; } } } @@ -4420,7 +4460,9 @@ fn is_full_span_column_subgrid(style: &crate::LayoutStyle) -> bool { || style.overflow_hidden || style.position == Some(taffy::Position::Absolute) || style.width != crate::Dimension::Auto - || style.justify_self.is_some_and(|value| value != taffy::AlignSelf::STRETCH) + || style + .justify_self + .is_some_and(|value| value.resolve_normal(taffy::AlignSelf::STRETCH) != taffy::AlignSelf::STRETCH) || style.margin_auto[1] || style.margin_auto[3] { @@ -6695,12 +6737,24 @@ where && !matches!(style.position, Some(taffy::Position::Absolute)) } taffy::Display::Grid => { - let child_align = taffy_tree - .style(node) - .ok() + let child_style = taffy_tree.style(node).ok(); + let horizontal = child_style .and_then(|child| child.justify_self) .or(parent_style.justify_items) - .unwrap_or(taffy::AlignItems::STRETCH); + .unwrap_or(taffy::AlignItems::NORMAL); + let vertical = child_style + .and_then(|child| child.align_self) + .or(parent_style.align_items) + .unwrap_or(taffy::AlignItems::NORMAL); + let normal = if child_style.is_some_and(|child| child.item_is_replaced) + || (child_style.is_some_and(|child| child.aspect_ratio.is_some()) + && vertical == taffy::AlignItems::STRETCH) + { + taffy::AlignItems::START + } else { + taffy::AlignItems::STRETCH + }; + let child_align = horizontal.resolve_normal(normal); child_align == taffy::AlignItems::STRETCH } taffy::Display::Flex @@ -6715,7 +6769,8 @@ where .ok() .and_then(|child| child.align_self) .or(parent_style.align_items) - .unwrap_or(taffy::AlignItems::STRETCH); + .unwrap_or(taffy::AlignItems::NORMAL) + .resolve_normal(taffy::AlignItems::STRETCH); child_align == taffy::AlignItems::STRETCH } _ => false, @@ -7095,6 +7150,57 @@ fn effective_container_type(style: &crate::LayoutStyle) -> crate::ContainerType } } +#[inline] +fn used_flex_alignment(value: Option, parent: Option) -> taffy::AlignSelf { + value + .or(parent) + .unwrap_or(taffy::AlignSelf::NORMAL) + .resolve_normal(taffy::AlignSelf::STRETCH) +} + +#[inline] +fn used_grid_alignments( + style: &crate::LayoutStyle, + parent: &crate::LayoutStyle, +) -> (taffy::AlignSelf, taffy::AlignSelf) { + let horizontal = style.justify_self.or(parent.justify_items).unwrap_or(taffy::AlignSelf::NORMAL); + let vertical = style.align_self.or(parent.align_items).unwrap_or(taffy::AlignSelf::NORMAL); + if style.has_replaced_sizing { + return ( + horizontal.resolve_normal(taffy::AlignSelf::START), + vertical.resolve_normal(taffy::AlignSelf::START), + ); + } + if style.aspect_ratio.is_none() { + return ( + horizontal.resolve_normal(taffy::AlignSelf::STRETCH), + vertical.resolve_normal(taffy::AlignSelf::STRETCH), + ); + } + + let horizontal_is_normal = horizontal == taffy::AlignSelf::NORMAL; + let vertical_is_normal = vertical == taffy::AlignSelf::NORMAL; + let horizontal = if horizontal_is_normal { + if !vertical_is_normal && vertical == taffy::AlignSelf::STRETCH { + taffy::AlignSelf::START + } else { + taffy::AlignSelf::STRETCH + } + } else { + horizontal + }; + let vertical = if vertical_is_normal { + if !horizontal_is_normal && horizontal != taffy::AlignSelf::STRETCH { + taffy::AlignSelf::STRETCH + } else { + taffy::AlignSelf::START + } + } else { + vertical + }; + (horizontal, vertical) +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ContainerAutoInlineSize { FillAvailable, @@ -7117,7 +7223,11 @@ fn container_auto_inline_size( style: &crate::LayoutStyle, styles: &HashMap, ) -> ContainerAutoInlineSize { - if style.is_inline_block || style.float.is_some() { + // Grid items are blockified and their float is ignored. Their original + // inline-block/float provenance must not suppress grid self-alignment. + if (style.is_inline_block || style.float.is_some()) + && !is_in_flow_grid_item(tree, id, style, styles) + { return ContainerAutoInlineSize::Intrinsic; } if matches!(style.position, Some(taffy::Position::Absolute)) { @@ -7155,11 +7265,8 @@ fn container_auto_inline_size( ); if row { ContainerAutoInlineSize::Intrinsic - } else if style.align_self.unwrap_or( - parent_style - .align_items - .unwrap_or(taffy::AlignItems::STRETCH), - ) == taffy::AlignSelf::STRETCH + } else if used_flex_alignment(style.align_self, parent_style.align_items) + == taffy::AlignSelf::STRETCH { ContainerAutoInlineSize::FillAvailable } else { @@ -7167,11 +7274,9 @@ fn container_auto_inline_size( } } crate::Display::Grid => { - if style.justify_self.unwrap_or( - parent_style - .justify_items - .unwrap_or(taffy::JustifyItems::STRETCH), - ) == taffy::AlignSelf::STRETCH + if used_grid_alignments(style, parent_style).0 == taffy::AlignSelf::STRETCH + && !style.margin_auto[1] + && !style.margin_auto[3] { ContainerAutoInlineSize::StretchedGridItem } else { @@ -7215,11 +7320,7 @@ fn container_auto_block_size( }; if parent_style.display == crate::Display::Grid - && style.align_self.unwrap_or( - parent_style - .align_items - .unwrap_or(taffy::AlignItems::STRETCH), - ) == taffy::AlignSelf::STRETCH + && used_grid_alignments(style, parent_style).1 == taffy::AlignSelf::STRETCH && !style.margin_auto[0] && !style.margin_auto[2] { @@ -7513,6 +7614,27 @@ fn resolve_deferred_flex_inline_sizes( true } +fn is_in_flow_grid_item( + tree: &DomTree, + id: NodeId, + style: &crate::LayoutStyle, + styles: &HashMap, +) -> bool { + if matches!(style.position, Some(taffy::Position::Absolute)) { + return false; + } + let mut parent = tree.get_node(id).and_then(|node| node.parent); + loop { + let Some(parent_id) = parent else { return false }; + let Some(parent_style) = styles.get(&parent_id) else { return false }; + if parent_style.display_contents { + parent = tree.get_node(parent_id).and_then(|node| node.parent); + continue; + } + return parent_style.display == crate::Display::Grid; + } +} + fn build( tree: &DomTree, id: NodeId, @@ -7572,19 +7694,7 @@ fn build( if matches!(style.max_width, crate::Dimension::Percent(_)) && matches!(style.min_width, crate::Dimension::Auto) { - let mut parent = node.parent; - let grid_item = loop { - let Some(parent_id) = parent else { break false }; - let Some(parent_style) = styles.get(&parent_id) else { - break false; - }; - if parent_style.display_contents { - parent = tree.get_node(parent_id).and_then(|node| node.parent); - continue; - } - break parent_style.display == crate::Display::Grid; - }; - if grid_item { + if is_in_flow_grid_item(tree, id, style, styles) { taffy_style.min_size.width = taffy::Dimension::length(0.0); } } @@ -7685,6 +7795,21 @@ 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 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 @@ -7762,6 +7887,7 @@ fn build( if matches!(style.width, crate::Dimension::Auto) && matches!(style.height, crate::Dimension::Auto) && !has_percentage_constraint + && !is_in_flow_grid_item(tree, id, style, styles) { let constrained = crate::inline::constrained_auto_replaced_size(width, height, style); @@ -8589,10 +8715,7 @@ fn needs_column_flex_text_fit_content_cap( return false; } - style - .align_self - .unwrap_or(parent_style.align_items.unwrap_or(taffy::AlignItems::STRETCH)) - != taffy::AlignSelf::STRETCH + used_flex_alignment(style.align_self, parent_style.align_items) != taffy::AlignSelf::STRETCH } /// Expand `display: contents` wrappers so their children partition into the diff --git a/crates/obscura-render/src/inline.rs b/crates/obscura-render/src/inline.rs index aacc6f5..cb1a4ec 100644 --- a/crates/obscura-render/src/inline.rs +++ b/crates/obscura-render/src/inline.rs @@ -1974,6 +1974,37 @@ pub(crate) fn is_replaced(local: &str) -> bool { ) } +/// Elements that use CSS's intrinsic replaced-size algorithm. This is +/// deliberately narrower than `is_replaced`: ordinary form controls are +/// atomic inline boxes, but grid `normal` stretches them like non-replaced +/// boxes in Chromium and Gecko. +pub(crate) fn has_replaced_sizing(local: &str) -> bool { + matches!( + local, + "img" | "canvas" | "video" | "audio" | "iframe" | "embed" + | "object" | "progress" | "meter" + ) +} + +/// HTML's default object size for replaced media whose intrinsic metadata is +/// not available yet. Canvas dimensions and decoded video metadata can replace +/// these defaults before layout when present. +pub(crate) fn default_replaced_intrinsic_size( + local: &str, + font_size: f32, + has_controls: bool, + has_resource: bool, +) -> Option<(f32, f32)> { + match local { + "canvas" | "video" | "iframe" | "object" => Some((300.0, 150.0)), + "embed" if has_resource => Some((300.0, 150.0)), + "audio" if has_controls => Some((300.0, 54.0)), + "progress" => Some((font_size * 10.0, font_size)), + "meter" => Some((font_size * 5.0, font_size)), + _ => None, + } +} + /// Is `cid` (and its whole subtree) inline-level, in-flow content safe to fold /// into a shaped buffer? Sets `has_text` if it contributes any non-whitespace /// text. Inline wrappers (``, ``, ``, ``, ``, ...) are diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index 984183f..fa4d506 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -534,6 +534,9 @@ pub struct LayoutStyle { /// missing dimension from the given one, so a `width:100%` image gets a /// real height instead of collapsing to zero. pub aspect_ratio: Option, + /// Whether the preferred ratio came from decoded intrinsic media and + /// therefore applies to the content box regardless of `box-sizing`. + pub aspect_ratio_is_intrinsic: bool, /// Fetched intrinsic CSS-pixel size for a replaced element. Kept alongside /// `aspect_ratio` so its taffy leaf can contribute a real min/max-content /// size when percentage dimensions are resolved through an auto-sized @@ -551,6 +554,11 @@ pub struct LayoutStyle { /// cannot be inferred from intrinsic dimensions because an unloaded or /// broken image remains replaced. pub(crate) is_replaced_box: bool, + /// Whether this element uses the intrinsic replaced-element sizing + /// algorithm. This is narrower than `is_replaced_box`: form controls such + /// as buttons are atomic inline boxes but still use ordinary grid `normal` + /// stretch behavior in Chromium and Gecko. + pub(crate) has_replaced_sizing: 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 @@ -1360,6 +1368,8 @@ fn read_node(tree: &TaffyTree, id: NodeId) -> NodeRect { pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style { let mut s = Style::DEFAULT; + s.item_is_replaced = style.has_replaced_sizing; + s.item_aspect_ratio_is_intrinsic = style.aspect_ratio_is_intrinsic; s.box_sizing = match style.box_sizing { BoxSizing::ContentBox => taffy::BoxSizing::ContentBox, BoxSizing::BorderBox => taffy::BoxSizing::BorderBox, diff --git a/crates/obscura-render/src/paint.rs b/crates/obscura-render/src/paint.rs index 36e89b1..9d70af4 100644 --- a/crates/obscura-render/src/paint.rs +++ b/crates/obscura-render/src/paint.rs @@ -813,6 +813,7 @@ fn css_color([r, g, b, a]: [u8; 4]) -> String { fn align_items_css(value: taffy::AlignItems) -> String { let keyword = match value.keyword { + taffy::AlignItemsKeyword::Normal => "normal", taffy::AlignItemsKeyword::Start => "start", taffy::AlignItemsKeyword::End => "end", taffy::AlignItemsKeyword::FlexStart => "flex-start", diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index b5bc6e5..70574aa 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -125,6 +125,11 @@ pub fn ua_style(tag: &str) -> LayoutStyle { } else if tag == "a" { style.color = Some([0, 0, 238, 255]); // blue style.underline = Some(true); // UA default: links are underlined + } else if tag == "iframe" { + // HTML's UA sheet gives frames a two-pixel inset border. The paint + // model does not distinguish inset yet, but border-box geometry still + // includes these edges around the 300x150 default object size. + style.border = Edges { top: 2.0, right: 2.0, bottom: 2.0, left: 2.0 }; } else if tag == "button" { // HTML buttons are inline-block controls whose anonymous inner // content is centered by the browser UA sheet. Keep the atomic outer @@ -829,6 +834,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { "aspect-ratio" => { style.aspect_ratio = parse_aspect_ratio(value); style.aspect_ratio_is_mapped = false; + style.aspect_ratio_is_intrinsic = false; } "margin" => apply_margin_shorthand(style, value), "margin-top" => set_margin_side(style, 0, value), @@ -2252,7 +2258,7 @@ fn self_alignment_value(value: &str) -> Option> { let normalized = value.trim().to_ascii_lowercase(); let alignment = match normalized.as_str() { "auto" => return Some(None), - "normal" => taffy::AlignSelf::STRETCH, + "normal" => taffy::AlignSelf::NORMAL, "start" | "self-start" => taffy::AlignSelf::START, "end" | "self-end" => taffy::AlignSelf::END, "flex-start" => taffy::AlignSelf::FLEX_START, @@ -6193,8 +6199,8 @@ mod tests { assert_eq!(reset.justify_self, None); let normal = compute_style("div", Some("align-self:normal;justify-self:normal")); - assert_eq!(normal.align_self, Some(taffy::AlignSelf::STRETCH)); - assert_eq!(normal.justify_self, Some(taffy::JustifySelf::STRETCH)); + assert_eq!(normal.align_self, Some(taffy::AlignSelf::NORMAL)); + assert_eq!(normal.justify_self, Some(taffy::JustifySelf::NORMAL)); let shorthand = compute_style("div", Some("place-self:safe center flex-end")); assert_eq!(shorthand.align_self, Some(taffy::AlignSelf::SAFE_CENTER)); diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs index e2c3c1e..9a9f409 100644 --- a/crates/obscura-render/tests/layout_test.rs +++ b/crates/obscura-render/tests/layout_test.rs @@ -3816,3 +3816,191 @@ fn closed_details_content_does_not_paint() { "closed details content must leave the page background untouched" ); } + +#[test] +fn grid_replaced_normal_and_explicit_stretch_match_browser_geometry() { + let tree = parse_html( + r#" + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "#, + ); + let ids = [ + "normal", + "min-inline", + "min-block", + "normal-inline", + "start", + "auto-margin", + "stretch-inline", + "stretch-block", + "stretch-both", + "definite-inline", + "definite-block", + "parent-stretch", + "parent-normal", + "intrinsic-border-box", + "intrinsic-content-box", + "authored-border-box", + "authored-content-box", + "conflicting-min-inline", + "conflicting-min-block", + ]; + let intrinsic: HashMap<_, _> = ids + .iter() + .map(|name| (tree.get_element_by_id(name).unwrap(), (100.0, 50.0))) + .collect(); + let layout = layout_dom_with_images(&tree, (800.0, 3000.0), &intrinsic); + let rect = |name| layout.rects[&tree.get_element_by_id(name).unwrap()]; + let size = |name| { + let rect = rect(name); + (rect.width, rect.height) + }; + + assert_eq!(size("normal"), (100.0, 50.0)); + assert_eq!(size("min-inline"), (150.0, 75.0)); + assert_eq!(size("min-block"), (200.0, 100.0)); + assert_eq!(size("normal-inline"), (150.0, 75.0)); + assert_eq!(size("start"), (150.0, 75.0)); + assert_eq!(size("auto-margin"), (150.0, 75.0)); + assert_eq!(size("stretch-inline"), (300.0, 150.0)); + assert_eq!(size("stretch-block"), (400.0, 200.0)); + assert_eq!(size("stretch-both"), (300.0, 200.0)); + assert_eq!(size("definite-inline"), (120.0, 200.0)); + assert_eq!(size("definite-block"), (300.0, 80.0)); + assert_eq!(size("parent-stretch"), (300.0, 150.0)); + assert_eq!(size("parent-normal"), (150.0, 75.0)); + assert_eq!(size("intrinsic-border-box"), (120.0, 75.0)); + assert_eq!(size("intrinsic-content-box"), (150.0, 90.0)); + assert_eq!(size("authored-border-box"), (120.0, 120.0)); + assert_eq!(size("authored-content-box"), (150.0, 150.0)); + assert_eq!(size("conflicting-min-inline"), (250.0, 80.0)); + assert_eq!(size("conflicting-min-block"), (120.0, 100.0)); + + let item = rect("auto-margin"); + let parent = tree + .get_node(tree.get_element_by_id("auto-margin").unwrap()) + .and_then(|node| node.parent) + .unwrap(); + assert_eq!(item.x - layout.rects[&parent].x, 150.0); +} + +#[test] +fn grid_replaced_classification_keeps_controls_stretched_and_media_natural() { + let tree = parse_html( + r#" + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "#, + ); + let layout = layout_dom(&tree, (800.0, 5000.0)); + let size = |name| { + let rect = layout.rects[&tree.get_element_by_id(name).unwrap()]; + (rect.width, rect.height) + }; + + assert_eq!(size("svg"), (400.0, 200.0)); + assert_eq!(size("svg-border-box"), (120.0, 75.0)); + assert_eq!(size("mapped-border-box"), (120.0, 75.0)); + for name in ["canvas", "video", "embed", "object"] { + assert_eq!(size(name), (300.0, 150.0), "{name}"); + } + assert_eq!(size("iframe"), (304.0, 154.0)); + assert_eq!(size("empty-embed"), (0.0, 0.0)); + assert_eq!(size("audio"), (0.0, 0.0)); + assert_eq!(size("audio-controls"), (300.0, 54.0)); + assert_eq!(size("progress"), (160.0, 16.0)); + assert_eq!(size("meter"), (80.0, 16.0)); + assert_eq!(size("image-input"), (100.0, 50.0)); + assert_eq!( + layout.styles[&tree.get_element_by_id("button").unwrap()].width, + obscura_render::Dimension::Auto, + "grid-normal stretch must preserve the button's auto inline size" + ); + for name in ["button", "text-input", "select"] { + assert_eq!(size(name), (400.0, 300.0), "{name}"); + } + let auto_button = layout.rects[&tree.get_element_by_id("auto-button").unwrap()]; + let auto_button_grid = layout.rects[&tree.get_element_by_id("auto-button-grid").unwrap()]; + assert!(auto_button.width > 20.0 && auto_button.width < 30.0); + assert_eq!(auto_button.height, 300.0); + assert_eq!(auto_button.x + auto_button.width, auto_button_grid.x + auto_button_grid.width); +} + +#[test] +fn grid_ordinary_aspect_ratio_preserves_normal_alignment_provenance() { + let tree = parse_html( + r#" + +
+
+
+
+
+
+
+
+
+ "#, + ); + let layout = layout_dom(&tree, (800.0, 2000.0)); + let size = |name| { + let rect = layout.rects[&tree.get_element_by_id(name).unwrap()]; + (rect.width, rect.height) + }; + + assert_eq!(size("normal"), (300.0, 150.0)); + assert_eq!(size("align-stretch"), (400.0, 200.0)); + assert_eq!(size("justify-stretch"), (300.0, 150.0)); + assert_eq!(size("both-stretch"), (300.0, 200.0)); + assert_eq!(size("both-normal"), (300.0, 150.0)); + assert_eq!(size("align-start"), (300.0, 150.0)); + assert_eq!(size("justify-start"), (400.0, 200.0)); + assert_eq!(size("parent-align-stretch"), (400.0, 200.0)); + assert_eq!(size("parent-justify-stretch"), (300.0, 150.0)); +} diff --git a/vendor/taffy/src/compute/flexbox.rs b/vendor/taffy/src/compute/flexbox.rs index d7363e3..c526323 100644 --- a/vendor/taffy/src/compute/flexbox.rs +++ b/vendor/taffy/src/compute/flexbox.rs @@ -19,6 +19,11 @@ use super::common::alignment::apply_alignment_fallback; #[cfg(feature = "content_size")] use super::common::content_size::compute_content_size_contribution; +#[inline] +fn resolve_flex_normal(alignment: AlignSelf) -> AlignSelf { + if alignment.keyword == AlignItemsKeyword::Normal { AlignSelf::STRETCH } else { alignment } +} + /// The intermediate results of a flexbox calculation for a single item struct FlexItem { /// The identifier for the associated node @@ -451,7 +456,7 @@ fn compute_constants( let box_sizing_adjustment = if style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO }; - let align_items = style.align_items().unwrap_or(AlignItems::STRETCH); + let align_items = resolve_flex_normal(style.align_items().unwrap_or(AlignItems::STRETCH)); let align_content = style.align_content().unwrap_or(AlignContent::STRETCH); let justify_content = style.justify_content(); let layout_direction = style.direction(); @@ -569,7 +574,7 @@ fn generate_anonymous_flex_items( border: child_style .border() .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)), - align_self: child_style.align_self().unwrap_or(constants.align_items), + align_self: resolve_flex_normal(child_style.align_self().unwrap_or(constants.align_items)), overflow: child_style.overflow(), scrollbar_width: child_style.scrollbar_width(), flex_grow: child_style.flex_grow(), @@ -1847,7 +1852,7 @@ fn align_flex_items_along_cross_axis( } } } - AlignItemsKeyword::Stretch => { + AlignItemsKeyword::Normal | AlignItemsKeyword::Stretch => { if constants.is_wrap_reverse ^ cross_axis_should_reverse { free_space } else { @@ -2182,7 +2187,7 @@ fn perform_absolute_layout_on_absolute_children( let overflow = child_style.overflow(); let scrollbar_width = child_style.scrollbar_width(); let aspect_ratio = child_style.aspect_ratio(); - let align_self = child_style.align_self().unwrap_or(constants.align_items); + let align_self = resolve_flex_normal(child_style.align_self().unwrap_or(constants.align_items)); let margin = child_style .margin() .map(|margin| margin.resolve_to_option(inset_relative_size.width, |val, basis| tree.calc(val, basis))); @@ -2453,11 +2458,23 @@ fn perform_absolute_layout_on_absolute_children( (AlignItemsKeyword::End, true) => { constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir) } - (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, false) + ( + AlignItemsKeyword::Normal + | AlignItemsKeyword::Baseline + | AlignItemsKeyword::Stretch + | AlignItemsKeyword::FlexStart, + false, + ) | (AlignItemsKeyword::FlexEnd, true) => { constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir) } - (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, true) + ( + AlignItemsKeyword::Normal + | AlignItemsKeyword::Baseline + | AlignItemsKeyword::Stretch + | AlignItemsKeyword::FlexStart, + true, + ) | (AlignItemsKeyword::FlexEnd, false) => { constants.container_size.cross(constants.dir) - constants.content_box_inset.cross_end(constants.dir) diff --git a/vendor/taffy/src/compute/grid/alignment.rs b/vendor/taffy/src/compute/grid/alignment.rs index 2a2af3f..9033dac 100644 --- a/vendor/taffy/src/compute/grid/alignment.rs +++ b/vendor/taffy/src/compute/grid/alignment.rs @@ -103,11 +103,12 @@ pub(super) fn align_and_position_item( let box_sizing_adjustment = if style.box_sizing() == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO }; + let aspect_ratio_adjustment = + if style.aspect_ratio_uses_content_box() { padding_border_size } else { box_sizing_adjustment }; let inherent_size = style .size() .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis)) - .maybe_apply_aspect_ratio(aspect_ratio) .maybe_add(box_sizing_adjustment); let min_size = style .min_size() @@ -122,26 +123,15 @@ pub(super) fn align_and_position_item( .maybe_apply_aspect_ratio(aspect_ratio) .maybe_add(box_sizing_adjustment); - // Resolve default alignment styles if they are set on neither the parent or the node itself - // Note: if the child has a preferred aspect ratio but neither width or height are set, then the width is stretched - // and the then height is calculated from the width according the aspect ratio - // See: https://www.w3.org/TR/css-grid-1/#grid-item-sizing - let alignment_styles = InBothAbsAxis { - horizontal: justify_self.or(container_alignment_styles.horizontal).unwrap_or_else(|| { - if inherent_size.width.is_some() { - AlignSelf::START - } else { - AlignSelf::STRETCH - } - }), - vertical: align_self.or(container_alignment_styles.vertical).unwrap_or_else(|| { - if inherent_size.height.is_some() || aspect_ratio.is_some() { - AlignSelf::START - } else { - AlignSelf::STRETCH - } - }), - }; + // Preserve `normal` provenance until both axes and the preferred aspect + // ratio are known. Explicit stretch in one axis can make the other normal + // axis ratio-derived, while two normal axes prefer inline-axis stretch. + let alignment_styles = super::resolve_item_alignment( + justify_self.or(container_alignment_styles.horizontal).unwrap_or(AlignSelf::NORMAL), + align_self.or(container_alignment_styles.vertical).unwrap_or(AlignSelf::NORMAL), + style.is_compressible_replaced(), + aspect_ratio.is_some(), + ); // Note: This is not a bug. It is part of the CSS spec that both horizontal and vertical margins // resolve against the WIDTH of the grid area. @@ -179,10 +169,7 @@ pub(super) fn align_and_position_item( None }); - // Reapply aspect ratio after stretch and absolute position width adjustments - let Size { width, height } = Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio); - - let height = height.or_else(|| { + let height = inherent_size.height.or_else(|| { if position == Position::Absolute { if let (Some(top), Some(bottom)) = (inset_vertical.start, inset_vertical.end) { return Some(f32_max(grid_area_minus_item_margins_size.height - top - bottom, 0.0)); @@ -203,8 +190,12 @@ pub(super) fn align_and_position_item( None }); - // Reapply aspect ratio after stretch and absolute position height adjustments - let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio); + // Stretch is resolved before preferred-ratio transfer. If both axes are + // stretched (or otherwise definite), the ratio does not overwrite either + // one. If only one axis is definite, it supplies the other through the + // ratio. This matches replaced grid sizing in Blink and Gecko. + let Size { width, height } = + super::apply_preferred_aspect_ratio(Size { width, height }, aspect_ratio, aspect_ratio_adjustment); // Clamp size by min and max width/height let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size); @@ -290,7 +281,7 @@ pub(super) fn align_and_position_item( let (x, x_margin) = align_item_within_area( Line { start: grid_area.left, end: grid_area.right }, - justify_self.unwrap_or(alignment_styles.horizontal), + alignment_styles.horizontal, width, position, inset_horizontal, @@ -300,7 +291,7 @@ pub(super) fn align_and_position_item( ); let (y, y_margin) = align_item_within_area( Line { start: grid_area.top, end: grid_area.bottom }, - align_self.unwrap_or(alignment_styles.vertical), + alignment_styles.vertical, height, position, inset_vertical, @@ -388,7 +379,8 @@ pub(super) fn align_item_within_area( // Compute offset in the axis let alignment_based_offset = match alignment_keyword { // TODO: Add support for baseline alignment. For now we treat it as "start". - AlignItemsKeyword::Start + AlignItemsKeyword::Normal + | AlignItemsKeyword::Start | AlignItemsKeyword::FlexStart | AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch => { @@ -444,6 +436,7 @@ pub(super) fn align_item_within_area( mod tests { use super::{f32_max, f32_min}; use crate::prelude::*; + use crate::Point; #[derive(Clone, Copy)] struct IntrinsicInlineSize { @@ -467,6 +460,46 @@ mod tests { Size { width, height: known_dimensions.height.unwrap_or(10.0) } } + #[derive(Clone, Copy)] + struct ReplacedIntrinsicSize { + width: f32, + height: f32, + } + + fn measure_replaced_child( + known_dimensions: Size>, + _available_space: Size, + _node: NodeId, + context: Option<&mut ReplacedIntrinsicSize>, + _style: &Style, + ) -> Size { + let intrinsic = context.copied().unwrap_or(ReplacedIntrinsicSize { width: 0.0, height: 0.0 }); + let ratio = intrinsic.width / intrinsic.height; + match known_dimensions { + Size { width: Some(width), height: Some(height) } => Size { width, height }, + Size { width: Some(width), height: None } => Size { width, height: width / ratio }, + Size { width: None, height: Some(height) } => Size { width: height * ratio, height }, + Size { width: None, height: None } => Size { width: intrinsic.width, height: intrinsic.height }, + } + } + + fn layout_replaced_grid_item(mut item_style: Style, mut root_style: Style) -> (Point, Size) { + let mut tree = TaffyTree::new(); + tree.disable_rounding(); + item_style.aspect_ratio = Some(2.0); + let item = tree + .new_leaf_with_context(item_style, ReplacedIntrinsicSize { width: 100.0, height: 50.0 }) + .unwrap(); + root_style.display = Display::Grid; + root_style.size = Size { width: length(300.0), height: length(200.0) }; + root_style.grid_template_columns = vec![length(300.0)]; + root_style.grid_template_rows = vec![length(200.0)]; + let root = tree.new_with_children(root_style, &[item]).unwrap(); + tree.compute_layout_with_measure(root, Size::MAX_CONTENT, measure_replaced_child).unwrap(); + let layout = tree.layout(item).unwrap(); + (layout.location, layout.size) + } + fn inline_margins(left_auto: bool, right_auto: bool) -> Rect { Rect { left: if left_auto { auto() } else { zero() }, @@ -626,6 +659,130 @@ mod tests { assert_eq!(content_box_max, 290.0); } + #[test] + fn grid_normal_uses_natural_replaced_size_but_stretches_ordinary_items() { + let (_, replaced) = layout_replaced_grid_item( + Style { item_is_replaced: true, ..Style::default() }, + Style::default(), + ); + assert_eq!(replaced, Size { width: 100.0, height: 50.0 }); + + let (_, ordinary) = layout_replaced_grid_item(Style::default(), Style::default()); + assert_eq!(ordinary, Size { width: 300.0, height: 150.0 }); + } + + #[test] + fn ordinary_grid_aspect_ratio_preserves_normal_alignment_provenance() { + let cases = [ + (None, None, None, None, Size { width: 300.0, height: 150.0 }), + (None, Some(AlignSelf::STRETCH), None, None, Size { width: 400.0, height: 200.0 }), + (Some(AlignSelf::STRETCH), None, None, None, Size { width: 300.0, height: 150.0 }), + ( + Some(AlignSelf::STRETCH), + Some(AlignSelf::STRETCH), + None, + None, + Size { width: 300.0, height: 200.0 }, + ), + (None, Some(AlignSelf::START), None, None, Size { width: 300.0, height: 150.0 }), + (Some(AlignSelf::START), None, None, None, Size { width: 400.0, height: 200.0 }), + (None, None, Some(AlignItems::STRETCH), None, Size { width: 400.0, height: 200.0 }), + (None, None, None, Some(AlignItems::STRETCH), Size { width: 300.0, height: 150.0 }), + ]; + for (justify_self, align_self, align_items, justify_items, expected) in cases { + let (_, actual) = layout_replaced_grid_item( + Style { justify_self, align_self, ..Style::default() }, + Style { align_items, justify_items, ..Style::default() }, + ); + assert_eq!(actual, expected, "justify={justify_self:?}, align={align_self:?}"); + } + } + + #[test] + fn replaced_grid_normal_applies_percentage_minimums_without_implicit_stretch() { + let (_, inline_min) = layout_replaced_grid_item( + Style { + item_is_replaced: true, + min_size: Size { width: percent(0.5), height: Dimension::auto() }, + ..Style::default() + }, + Style::default(), + ); + assert_eq!(inline_min, Size { width: 150.0, height: 75.0 }); + + let (_, block_min) = layout_replaced_grid_item( + Style { + item_is_replaced: true, + min_size: Size { width: Dimension::auto(), height: percent(0.5) }, + ..Style::default() + }, + Style::default(), + ); + assert_eq!(block_min, Size { width: 200.0, height: 100.0 }); + } + + #[test] + fn replaced_grid_explicit_stretch_precedes_aspect_ratio_transfer() { + let base = Style { item_is_replaced: true, ..Style::default() }; + + let (_, inline_stretch) = layout_replaced_grid_item( + Style { justify_self: Some(AlignSelf::STRETCH), ..base.clone() }, + Style::default(), + ); + assert_eq!(inline_stretch, Size { width: 300.0, height: 150.0 }); + + let (_, block_stretch) = layout_replaced_grid_item( + Style { align_self: Some(AlignSelf::STRETCH), ..base.clone() }, + Style::default(), + ); + assert_eq!(block_stretch, Size { width: 400.0, height: 200.0 }); + + let (_, both_stretch) = layout_replaced_grid_item( + Style { + justify_self: Some(AlignSelf::STRETCH), + align_self: Some(AlignSelf::STRETCH), + ..base.clone() + }, + Style::default(), + ); + assert_eq!(both_stretch, Size { width: 300.0, height: 200.0 }); + + let (_, definite_inline) = layout_replaced_grid_item( + Style { + size: Size { width: length(120.0), height: Dimension::auto() }, + align_self: Some(AlignSelf::STRETCH), + ..base.clone() + }, + Style::default(), + ); + assert_eq!(definite_inline, Size { width: 120.0, height: 200.0 }); + + let (_, definite_block) = layout_replaced_grid_item( + Style { + size: Size { width: Dimension::auto(), height: length(80.0) }, + justify_self: Some(AlignSelf::STRETCH), + ..base + }, + Style::default(), + ); + assert_eq!(definite_block, Size { width: 300.0, height: 80.0 }); + } + + #[test] + fn replaced_grid_normal_and_auto_margins_remain_natural() { + let (location, size) = layout_replaced_grid_item( + Style { + item_is_replaced: true, + min_size: Size { width: percent(0.5), height: Dimension::auto() }, + margin: Rect { left: auto(), right: zero(), top: zero(), bottom: zero() }, + ..Style::default() + }, + Style { justify_items: Some(AlignItems::NORMAL), ..Style::default() }, + ); + assert_eq!(size, Size { width: 150.0, height: 75.0 }); + assert_eq!(location.x, 150.0); + } + #[test] fn overflowing_auto_margins_override_unsafe_self_alignment() { let area = Line { start: 0.0, end: 300.0 }; diff --git a/vendor/taffy/src/compute/grid/mod.rs b/vendor/taffy/src/compute/grid/mod.rs index 1a631a8..1967db4 100644 --- a/vendor/taffy/src/compute/grid/mod.rs +++ b/vendor/taffy/src/compute/grid/mod.rs @@ -34,6 +34,80 @@ mod track_sizing; mod types; mod util; +/// Resolve grid item `normal` alignment without losing the authored/default +/// provenance needed by preferred-aspect-ratio sizing. With an aspect ratio, +/// one implicit stretch axis supplies the other; an explicit alignment in the +/// opposite axis decides which implicit axis remains stretchable. +#[inline] +fn resolve_item_alignment( + horizontal: AlignSelf, + vertical: AlignSelf, + is_compressible_replaced: bool, + has_preferred_aspect_ratio: bool, +) -> InBothAbsAxis { + if is_compressible_replaced { + return InBothAbsAxis { + horizontal: horizontal.resolve_normal(AlignSelf::START), + vertical: vertical.resolve_normal(AlignSelf::START), + }; + } + if !has_preferred_aspect_ratio { + return InBothAbsAxis { + horizontal: horizontal.resolve_normal(AlignSelf::STRETCH), + vertical: vertical.resolve_normal(AlignSelf::STRETCH), + }; + } + + let horizontal_is_normal = horizontal == AlignSelf::NORMAL; + let vertical_is_normal = vertical == AlignSelf::NORMAL; + InBothAbsAxis { + horizontal: if horizontal_is_normal { + if !vertical_is_normal && vertical == AlignSelf::STRETCH { + AlignSelf::START + } else { + AlignSelf::STRETCH + } + } else { + horizontal + }, + vertical: if vertical_is_normal { + if !horizontal_is_normal && horizontal != AlignSelf::STRETCH { + AlignSelf::STRETCH + } else { + AlignSelf::START + } + } else { + vertical + }, + } +} + +#[inline] +fn apply_preferred_aspect_ratio( + size: Size>, + aspect_ratio: Option, + box_sizing_adjustment: Size, +) -> Size> { + let Some(aspect_ratio) = aspect_ratio else { return size }; + match size { + Size { width: Some(width), height: None } => Size { + width: Some(width), + height: Some( + f32_max(width - box_sizing_adjustment.width, 0.0) / aspect_ratio + + box_sizing_adjustment.height, + ), + }, + Size { width: None, height: Some(height) } => Size { + width: Some( + f32_max(height - box_sizing_adjustment.height, 0.0) * aspect_ratio + + box_sizing_adjustment.width, + ), + height: Some(height), + }, + _ => size, + } +} + /// Grid layout algorithm /// This consists of a few phases: /// - Resolving the explicit grid @@ -225,8 +299,8 @@ pub fn compute_grid_layout( in_flow_children_iter, direction, style.grid_auto_flow(), - align_items.unwrap_or(AlignItems::STRETCH), - justify_items.unwrap_or(AlignItems::STRETCH), + align_items.unwrap_or(AlignItems::NORMAL), + justify_items.unwrap_or(AlignItems::NORMAL), &name_resolver, ); diff --git a/vendor/taffy/src/compute/grid/types/grid_item.rs b/vendor/taffy/src/compute/grid/types/grid_item.rs index d407d0c..f27d578 100644 --- a/vendor/taffy/src/compute/grid/types/grid_item.rs +++ b/vendor/taffy/src/compute/grid/types/grid_item.rs @@ -45,6 +45,8 @@ pub(in super::super) struct GridItem { pub max_size: Size, /// The item's aspect_ratio style pub aspect_ratio: Option, + /// Intrinsic ratios apply to the content box even under border-box sizing. + pub aspect_ratio_uses_content_box: bool, /// The item's padding style pub padding: Rect, /// The item's border style @@ -104,12 +106,13 @@ impl GridItem { parent_justify_items: AlignItems, source_order: u16, ) -> Self { + let is_compressible_replaced = style.is_compressible_replaced(); GridItem { node, source_order, row: row_span, column: col_span, - is_compressible_replaced: style.is_compressible_replaced(), + is_compressible_replaced, intrinsic_size_containment: style.intrinsic_size_containment(), overflow: style.overflow(), box_sizing: style.box_sizing(), @@ -117,6 +120,7 @@ impl GridItem { min_size: style.min_size(), max_size: style.max_size(), aspect_ratio: style.aspect_ratio(), + aspect_ratio_uses_content_box: style.aspect_ratio_uses_content_box(), padding: style.padding(), border: style.border(), margin: style.margin(), @@ -263,10 +267,14 @@ impl GridItem { let padding_border_size = (padding + border).sum_axes(); let box_sizing_adjustment = if self.box_sizing == BoxSizing::ContentBox { padding_border_size } else { Size::ZERO }; + let aspect_ratio_adjustment = if self.aspect_ratio_uses_content_box { + padding_border_size + } else { + box_sizing_adjustment + }; let inherent_size = self .size .maybe_resolve(grid_area_size, |val, basis| tree.calc(val, basis)) - .maybe_apply_aspect_ratio(aspect_ratio) .maybe_add(box_sizing_adjustment); let min_size = self .min_size @@ -280,6 +288,12 @@ impl GridItem { .maybe_add(box_sizing_adjustment); let grid_area_minus_item_margins_size = grid_area_size.maybe_sub(margins); + let alignment = super::super::resolve_item_alignment( + self.justify_self, + self.align_self, + self.is_compressible_replaced, + aspect_ratio.is_some(), + ); // If node is absolutely positioned and width is not set explicitly, then deduce it // from left, right and container_content_box if both are set. @@ -288,29 +302,34 @@ impl GridItem { // - Alignment style is "stretch" // - The node is not absolutely positioned // - The node does not have auto margins in this axis. - if !self.margin.left.is_auto() && !self.margin.right.is_auto() && self.justify_self == AlignSelf::STRETCH { + if !self.margin.left.is_auto() + && !self.margin.right.is_auto() + && alignment.horizontal == AlignSelf::STRETCH + { return grid_area_minus_item_margins_size.width; } None }); - // Reapply aspect ratio after stretch and absolute position width adjustments - let Size { width, height } = - Size { width, height: inherent_size.height }.maybe_apply_aspect_ratio(aspect_ratio); - - let height = height.or_else(|| { + let height = inherent_size.height.or_else(|| { // Apply height based on stretch alignment if: // - Alignment style is "stretch" // - The node is not absolutely positioned // - The node does not have auto margins in this axis. - if !self.margin.top.is_auto() && !self.margin.bottom.is_auto() && self.align_self == AlignSelf::STRETCH { + if !self.margin.top.is_auto() + && !self.margin.bottom.is_auto() + && alignment.vertical == AlignSelf::STRETCH + { return grid_area_minus_item_margins_size.height; } None }); - // Reapply aspect ratio after stretch and absolute position height adjustments - let Size { width, height } = Size { width, height }.maybe_apply_aspect_ratio(aspect_ratio); + let Size { width, height } = super::super::apply_preferred_aspect_ratio( + Size { width, height }, + aspect_ratio, + aspect_ratio_adjustment, + ); // Clamp size by min and max width/height let Size { width, height } = Size { width, height }.maybe_clamp(min_size, max_size); diff --git a/vendor/taffy/src/style/alignment.rs b/vendor/taffy/src/style/alignment.rs index eda57a0..fa728de 100644 --- a/vendor/taffy/src/style/alignment.rs +++ b/vendor/taffy/src/style/alignment.rs @@ -20,6 +20,8 @@ use crate::util::parse::{CssParseResult, FromCss, Parser, Token}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[repr(u8)] pub enum AlignItemsKeyword { + /// Use the layout mode's normal alignment behavior. + Normal, /// Items are packed toward the start of the axis. Start, /// Items are packed toward the end of the axis. @@ -131,6 +133,8 @@ pub struct AlignItems { } impl AlignItems { + /// Use the layout mode's normal alignment behavior. + pub const NORMAL: Self = Self { keyword: AlignItemsKeyword::Normal, safety: AlignmentSafety::Unsafe }; /// Items are packed toward the start of the axis. pub const START: Self = Self { keyword: AlignItemsKeyword::Start, safety: AlignmentSafety::Unsafe }; /// Items are packed toward the end of the axis. @@ -172,6 +176,12 @@ impl AlignItems { pub const fn keyword(self) -> AlignItemsKeyword { self.keyword } + + /// Resolve the context-dependent `normal` keyword to its used alignment. + #[inline] + pub const fn resolve_normal(self, normal: Self) -> Self { + if matches!(self.keyword, AlignItemsKeyword::Normal) { normal } else { self } + } } #[cfg(feature = "parse")] @@ -201,6 +211,7 @@ impl FromCss for AlignItems { _ => Err(input.new_unexpected_token_error(Token::Ident(pos))), } }, + "normal" => Ok(Self::NORMAL), "start" => Ok(Self::START), "end" => Ok(Self::END), "flex-start" => Ok(Self::FLEX_START), @@ -362,6 +373,7 @@ pub type JustifyContent = AlignContent; /// `unknown_variant` errors. Mirrors the spellings produced by `Serialize`. #[cfg(feature = "serde")] const ALIGN_ITEMS_NAMES: &[&str] = &[ + "Normal", "Start", "End", "FlexStart", @@ -380,6 +392,7 @@ const ALIGN_ITEMS_NAMES: &[&str] = &[ impl serde::Serialize for AlignItems { fn serialize(&self, serializer: S) -> Result { let name = match (self.keyword, self.safety) { + (AlignItemsKeyword::Normal, _) => "Normal", (AlignItemsKeyword::Start, AlignmentSafety::Unsafe) => "Start", (AlignItemsKeyword::End, AlignmentSafety::Unsafe) => "End", (AlignItemsKeyword::FlexStart, AlignmentSafety::Unsafe) => "FlexStart", @@ -408,6 +421,7 @@ impl<'de> serde::Deserialize<'de> for AlignItems { } fn visit_str(self, v: &str) -> Result { Ok(match v { + "Normal" => AlignItems::NORMAL, "Start" => AlignItems::START, "End" => AlignItems::END, "FlexStart" => AlignItems::FLEX_START, diff --git a/vendor/taffy/src/style/mod.rs b/vendor/taffy/src/style/mod.rs index 00b53d6..e02870e 100644 --- a/vendor/taffy/src/style/mod.rs +++ b/vendor/taffy/src/style/mod.rs @@ -99,6 +99,12 @@ pub trait CoreStyle { fn is_compressible_replaced(&self) -> bool { false } + /// Whether the preferred aspect ratio is intrinsic and therefore always + /// applies to the content box, independently of `box-sizing`. + #[inline(always)] + fn aspect_ratio_uses_content_box(&self) -> bool { + false + } /// Which box do size styles apply to #[inline(always)] fn box_sizing(&self) -> BoxSizing { @@ -442,6 +448,8 @@ pub struct Style { /// Is it a replaced element like an image or form field? /// pub item_is_replaced: bool, + /// Whether `aspect_ratio` came from intrinsic media metadata. + pub item_aspect_ratio_is_intrinsic: bool, /// Whether descendants are ignored for intrinsic size contributions in each axis. /// /// Ordinary final layout is unaffected, allowing an auto-sized grid item to @@ -596,6 +604,7 @@ impl Style { display: Display::DEFAULT, item_is_table: false, item_is_replaced: false, + item_aspect_ratio_is_intrinsic: false, intrinsic_size_containment: Size { width: false, height: false }, box_sizing: BoxSizing::BorderBox, direction: Direction::Ltr, @@ -693,6 +702,10 @@ impl CoreStyle for Style { self.item_is_replaced } #[inline(always)] + fn aspect_ratio_uses_content_box(&self) -> bool { + self.item_aspect_ratio_is_intrinsic + } + #[inline(always)] fn box_sizing(&self) -> BoxSizing { self.box_sizing } @@ -762,6 +775,10 @@ impl CoreStyle for &'_ T { (*self).is_compressible_replaced() } #[inline(always)] + fn aspect_ratio_uses_content_box(&self) -> bool { + (*self).aspect_ratio_uses_content_box() + } + #[inline(always)] fn box_sizing(&self) -> BoxSizing { (*self).box_sizing() } @@ -1222,6 +1239,7 @@ mod tests { display: Default::default(), item_is_table: false, item_is_replaced: false, + item_aspect_ratio_is_intrinsic: false, intrinsic_size_containment: Size { width: false, height: false }, box_sizing: Default::default(), #[cfg(feature = "float_layout")]