diff --git a/crates/css/src/values.rs b/crates/css/src/values.rs index 2531e1f..811b158 100644 --- a/crates/css/src/values.rs +++ b/crates/css/src/values.rs @@ -49,6 +49,15 @@ pub enum CssValue { Math(Box), /// A parsed CSS transform scale() function. TransformScale(f64, f64), + /// A parsed CSS transform translate*() function. + TransformTranslate(TransformLength, TransformLength), +} + +/// One translate component in a CSS transform. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TransformLength { + Px(f64), + Percent(f64), } /// Direction of a parsed `linear-gradient(...)`. @@ -313,6 +322,10 @@ fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { "max" => parse_math_max(args), "clamp" => parse_math_clamp(args), "scale" => parse_transform_scale(args), + "translate" => parse_transform_translate(args), + "translate3d" => parse_transform_translate3d(args), + "translatex" => parse_transform_translate_axis(args, true), + "translatey" => parse_transform_translate_axis(args, false), _ => CssValue::Keyword(format!("{name}()")), } } @@ -421,6 +434,51 @@ fn parse_transform_scale(args: &[ComponentValue]) -> CssValue { } } +fn parse_transform_translate(args: &[ComponentValue]) -> CssValue { + let values: Vec = args.iter().filter_map(parse_transform_length_arg).collect(); + + match values.as_slice() { + [x] => CssValue::TransformTranslate(*x, TransformLength::Px(0.0)), + [x, y] => CssValue::TransformTranslate(*x, *y), + _ => CssValue::Keyword("translate()".to_string()), + } +} + +fn parse_transform_translate3d(args: &[ComponentValue]) -> CssValue { + let values: Vec = args.iter().filter_map(parse_transform_length_arg).collect(); + + match values.as_slice() { + [x, y, _z] => CssValue::TransformTranslate(*x, *y), + _ => CssValue::Keyword("translate3d()".to_string()), + } +} + +fn parse_transform_translate_axis(args: &[ComponentValue], x_axis: bool) -> CssValue { + let values: Vec = args.iter().filter_map(parse_transform_length_arg).collect(); + + match values.as_slice() { + [value] if x_axis => CssValue::TransformTranslate(*value, TransformLength::Px(0.0)), + [value] => CssValue::TransformTranslate(TransformLength::Px(0.0), *value), + _ => CssValue::Keyword(if x_axis { + "translateX()".to_string() + } else { + "translateY()".to_string() + }), + } +} + +fn parse_transform_length_arg(arg: &ComponentValue) -> Option { + match arg { + ComponentValue::Number(n, _) if *n == 0.0 => Some(TransformLength::Px(0.0)), + ComponentValue::Percentage(n) => Some(TransformLength::Percent(*n)), + ComponentValue::Dimension(n, _, unit) if unit.eq_ignore_ascii_case("px") => { + Some(TransformLength::Px(*n)) + } + ComponentValue::Whitespace | ComponentValue::Comma => None, + _ => None, + } +} + fn parse_url(args: &[ComponentValue]) -> CssValue { for arg in args { match arg { @@ -2186,6 +2244,27 @@ mod tests { ); } + #[test] + fn test_parse_transform_translate3d_from_css_text() { + let val = parse_value(&parse_cv("translate3d(-50%, -25%, 0)")); + assert_eq!( + val, + CssValue::TransformTranslate( + TransformLength::Percent(-50.0), + TransformLength::Percent(-25.0), + ) + ); + } + + #[test] + fn test_parse_transform_translate_axis_from_css_text() { + let val = parse_value(&parse_cv("translateX(12px)")); + assert_eq!( + val, + CssValue::TransformTranslate(TransformLength::Px(12.0), TransformLength::Px(0.0)) + ); + } + #[test] fn test_shorthand_from_css_text() { use crate::parser::Parser; diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 71402d6..78df049 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -3266,6 +3266,9 @@ fn measure_box_content_width(b: &LayoutBox, font: &Font, max_w: &mut f32) { } else { let mut child_max = 0.0f32; for child in &b.children { + if !is_in_flow(child) { + continue; + } measure_box_content_width(child, font, &mut child_max); } child_max @@ -7905,6 +7908,46 @@ p { margin-top: 50px; margin-bottom: 50px; } ); } + #[test] + fn text_align_right_positions_inline_block_before_block_sibling() { + let html_str = r#" + + + + + +
Info
+ +"#; + let doc = we_html::parse_html(html_str); + let tree = layout_doc(&doc); + + let body_box = &tree.root.children[0]; + let container = &body_box.children[0]; + let inline_run = &container.children[0]; + let badge = &inline_run.children[0]; + + assert!(matches!(inline_run.box_type, BoxType::Anonymous)); + assert!( + (badge.rect.x - 480.0).abs() < 1.0, + "right-aligned inline-block should ignore abspos descendants when shrink-wrapping, got x={} width={}", + badge.rect.x, + badge.rect.width + ); + assert!( + badge.rect.height < 60.0, + "inline-block height should ignore tall abspos descendants, got {}", + badge.rect.height + ); + } + #[test] fn inline_padding_offsets_text() { let html_str = r#" diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 2dda5e7..787e305 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -314,8 +314,12 @@ fn paint_box( } let visible = layout_box.visibility == Visibility::Visible; - let tx = translate.0; - let ty = translate.1; + let transform_ref = border_box(layout_box); + let (transform_tx, transform_ty) = layout_box + .transform + .paint_offset(transform_ref.width, transform_ref.height); + let tx = translate.0 + transform_tx; + let ty = translate.1 + transform_ty; // If this box needs a compositing layer, wrap all its paint commands. let layer = needs_compositing_layer(layout_box); @@ -417,13 +421,13 @@ fn paint_box( } // Compute child translate: adds scroll offset for scrollable boxes. - let mut child_translate = translate; + let mut child_translate = (tx, ty); // When entering a scroll container, update the sticky reference point // to the container's padding box top in screen coordinates (pre-scroll). let mut child_sticky_ref = sticky_ref_screen_y; if scrollable { // The scroll container's padding box top on screen (before scroll). - child_sticky_ref = (layout_box.rect.y - layout_box.padding.top) + translate.1; + child_sticky_ref = (layout_box.rect.y - layout_box.padding.top) + ty; if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { if let Some(&(sx, sy)) = scroll_state.get(&node_id) { child_translate.0 -= sx; @@ -2553,6 +2557,36 @@ body { margin: 0; } assert!(has_fill(Color::rgb(0, 128, 0))); } + #[test] + fn transform_translate_offsets_subtree_paint() { + let html_str = r#" + +
"#; + let doc = we_html::parse_html(html_str); + let tree = layout_doc(&doc); + let list = build_display_list(&tree); + + let child_rect = list + .iter() + .find_map(|cmd| match cmd { + PaintCommand::FillRect { + x, + y, + width, + height, + color, + } if *color == Color::rgb(0, 0, 255) => Some((*x, *y, *width, *height)), + _ => None, + }) + .expect("child fill rect"); + + assert_eq!(child_rect, (25.0, 10.0, 30.0, 20.0)); + } + #[test] fn canvas_background_propagates_from_body() { // example.com pattern: `body { background:#eee }`, html transparent. diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index 231cf28..67ed9de 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -21,6 +21,7 @@ use we_css::transitions::{ }; use we_css::values::{ expand_shorthand, parse_value, Color, CssValue, LengthUnit, LinearGradient, MathExpr, + TransformLength as CssTransformLength, }; use we_dom::{Document, NodeData, NodeId}; use we_memory::intern::Atom; @@ -356,13 +357,45 @@ pub enum Transform { x: f32, y: f32, }, + Translate { + x: TransformLength, + y: TransformLength, + }, } impl Transform { pub fn paints_as_zero_scale(self) -> bool { match self { Transform::Scale { x, y } => x == 0.0 || y == 0.0, - Transform::None => false, + Transform::None | Transform::Translate { .. } => false, + } + } + + pub fn paint_offset(self, width: f32, height: f32) -> (f32, f32) { + match self { + Transform::Translate { x, y } => (x.resolve(width), y.resolve(height)), + Transform::None | Transform::Scale { .. } => (0.0, 0.0), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TransformLength { + Px(f32), + Percent(f32), +} + +impl Default for TransformLength { + fn default() -> Self { + TransformLength::Px(0.0) + } +} + +impl TransformLength { + fn resolve(self, reference: f32) -> f32 { + match self { + TransformLength::Px(px) => px, + TransformLength::Percent(percent) => percent / 100.0 * reference, } } } @@ -1852,11 +1885,35 @@ fn parse_transform(value: &CssValue) -> Option { x: *x as f32, y: *y as f32, }), - CssValue::List(values) => values.iter().find_map(parse_transform), + CssValue::TransformTranslate(x, y) => Some(Transform::Translate { + x: css_transform_length(*x), + y: css_transform_length(*y), + }), + CssValue::List(values) => { + let mut translate = None; + for value in values { + if let Some(transform) = parse_transform(value) { + if transform.paints_as_zero_scale() { + return Some(transform); + } + if matches!(transform, Transform::Translate { .. }) && translate.is_none() { + translate = Some(transform); + } + } + } + translate + } _ => None, } } +fn css_transform_length(value: CssTransformLength) -> TransformLength { + match value { + CssTransformLength::Px(px) => TransformLength::Px(px as f32), + CssTransformLength::Percent(percent) => TransformLength::Percent(percent as f32), + } +} + // --------------------------------------------------------------------------- // Apply a single property value to a ComputedStyle // --------------------------------------------------------------------------- @@ -2407,9 +2464,8 @@ fn apply_property( }; } - // Transform. Only scale() is represented today; non-zero scales are - // preserved for later transform support, while scale(0) suppresses - // painting in the renderer. + // Transform. We represent the pieces the renderer currently consumes: + // translate*() paint offsets and scale(0) paint suppression. "transform" => { if let Some(transform) = parse_transform(value) { style.transform = transform; @@ -5315,6 +5371,45 @@ mod tests { assert!(!wide.style.transform.paints_as_zero_scale()); } + #[test] + fn transform_translate_parsing() { + let html_str = r#" + +
A
B
"#; + let doc = we_html::parse_html(html_str); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let body = &styled.children[0]; + let centered = &body.children[0]; + let mixed = &body.children[1]; + let hidden = &body.children[2]; + + assert_eq!( + centered.style.transform, + Transform::Translate { + x: TransformLength::Percent(-50.0), + y: TransformLength::Percent(-25.0), + } + ); + assert_eq!( + centered.style.transform.paint_offset(200.0, 80.0), + (-100.0, -20.0) + ); + assert_eq!( + mixed.style.transform, + Transform::Translate { + x: TransformLength::Px(8.0), + y: TransformLength::Px(4.0), + } + ); + assert_eq!(hidden.style.transform, Transform::Scale { x: 0.0, y: 0.0 }); + assert!(hidden.style.transform.paints_as_zero_scale()); + } + #[test] fn appearance_none_parsing() { let html_str = r#"