From 415b7d87a96c5fd937d4e846d8bc02848af3c65c Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Thu, 16 Jul 2026 15:30:25 +0800 Subject: [PATCH] Improve Bing search chrome rendering for isu issue 280 Parse scale() transforms through computed style and suppress zero-scale subtrees during paint. Honor appearance:none for text inputs so author-styled search fields skip fallback native chrome while preserving default controls. Also apply clippy question_mark cleanups required by the workspace warning gate.\n\nIssue 280 remains open because the scenario still fails on existing tracked blockers: isu issue 305 and isu issues 378-380. --- crates/css/src/parser.rs | 16 ++--- crates/css/src/values.rs | 21 ++++++ crates/dom/src/validation.rs | 17 ++--- crates/js/src/dom_bridge.rs | 5 +- crates/layout/src/lib.rs | 16 +++-- crates/render/src/lib.rs | 81 +++++++++++++++++++++- crates/style/src/computed.rs | 127 +++++++++++++++++++++++++++++++++++ 7 files changed, 249 insertions(+), 34 deletions(-) diff --git a/crates/css/src/parser.rs b/crates/css/src/parser.rs index 96a87ec..f88c62e 100644 --- a/crates/css/src/parser.rs +++ b/crates/css/src/parser.rs @@ -741,12 +741,9 @@ impl Parser { } // Parse compound selector. - if let Some(compound) = self.parse_compound_selector() { - components.push(SelectorComponent::Compound(compound)); - expecting_compound = false; - } else { - return None; - } + let compound = self.parse_compound_selector()?; + components.push(SelectorComponent::Compound(compound)); + expecting_compound = false; } if components.is_empty() || expecting_compound { @@ -792,11 +789,8 @@ impl Parser { // Attribute Token::LeftBracket => { self.advance(); - if let Some(attr) = self.parse_attribute_selector() { - simple.push(SimpleSelector::Attribute(attr)); - } else { - return None; - } + let attr = self.parse_attribute_selector()?; + simple.push(SimpleSelector::Attribute(attr)); } // Pseudo-class Token::Colon => { diff --git a/crates/css/src/values.rs b/crates/css/src/values.rs index 89fcbe2..5dd50e2 100644 --- a/crates/css/src/values.rs +++ b/crates/css/src/values.rs @@ -45,6 +45,8 @@ pub enum CssValue { List(Vec), /// A CSS math function expression: calc(), min(), max(), clamp(). Math(Box), + /// A parsed CSS transform scale() function. + TransformScale(f64, f64), } // --------------------------------------------------------------------------- @@ -279,10 +281,29 @@ fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { "min" => parse_math_min(args), "max" => parse_math_max(args), "clamp" => parse_math_clamp(args), + "scale" => parse_transform_scale(args), _ => CssValue::Keyword(format!("{name}()")), } } +fn parse_transform_scale(args: &[ComponentValue]) -> CssValue { + let values: Vec = args + .iter() + .filter_map(|arg| match arg { + ComponentValue::Number(n, _) => Some(*n), + ComponentValue::Percentage(n) => Some(*n / 100.0), + ComponentValue::Whitespace | ComponentValue::Comma => None, + _ => None, + }) + .collect(); + + match values.as_slice() { + [x] => CssValue::TransformScale(*x, *x), + [x, y] => CssValue::TransformScale(*x, *y), + _ => CssValue::Keyword("scale()".to_string()), + } +} + fn parse_url(args: &[ComponentValue]) -> CssValue { for arg in args { match arg { diff --git a/crates/dom/src/validation.rs b/crates/dom/src/validation.rs index 3638a76..31e237a 100644 --- a/crates/dom/src/validation.rs +++ b/crates/dom/src/validation.rs @@ -901,11 +901,7 @@ fn match_piece( let mut cur = pos; for _ in 0..min { - if let Some(new_pos) = match_node(&piece.node, chars, cur) { - cur = new_pos; - } else { - return None; - } + cur = match_node(&piece.node, chars, cur)?; } positions.push(cur); @@ -943,14 +939,11 @@ fn match_quantified_group( let mut cur = pos; for _ in 0..min { - if let Some(new_pos) = match_expr(inner, chars, cur) { - if new_pos == cur { - break; // Avoid infinite loop on zero-length match. - } - cur = new_pos; - } else { - return None; + let new_pos = match_expr(inner, chars, cur)?; + if new_pos == cur { + break; // Avoid infinite loop on zero-length match. } + cur = new_pos; } positions.push(cur); diff --git a/crates/js/src/dom_bridge.rs b/crates/js/src/dom_bridge.rs index 7d35edf..a0a6687 100644 --- a/crates/js/src/dom_bridge.rs +++ b/crates/js/src/dom_bridge.rs @@ -281,10 +281,9 @@ fn parse_font_string(s: &str) -> Option { } else if let Some(n) = size_token.strip_suffix("em") { // em relative to parent — treat 1em = 16px as a default n.parse::().ok()? * 16.0 - } else if let Some(n) = size_token.strip_suffix("rem") { - n.parse::().ok()? * 16.0 } else { - return None; + let n = size_token.strip_suffix("rem")?; + n.parse::().ok()? * 16.0 }; if size_px <= 0.0 || !size_px.is_finite() { diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index dcec5cb..ee9965c 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -9,11 +9,11 @@ use std::collections::{HashMap, HashSet}; use we_css::values::Color; use we_dom::{Document, NodeData, NodeId}; use we_style::computed::{ - AlignContent, AlignItems, AlignSelf, BackgroundRepeat, BackgroundSize, BorderCollapse, - BorderStyle, BoxSizing, Clear, ComputedStyle, Cursor, Display, FlexDirection, FlexWrap, Float, - FontStyle, GridAutoFlow, GridPlacement, GridTrackSize, JustifyContent, JustifyItems, - JustifySelf, LengthOrAuto, Overflow, Position, StyledNode, TextAlign, TextDecoration, - TextOverflow, Visibility, WhiteSpace, WillChange, + AlignContent, AlignItems, AlignSelf, Appearance, BackgroundRepeat, BackgroundSize, + BorderCollapse, BorderStyle, BoxSizing, Clear, ComputedStyle, Cursor, Display, FlexDirection, + FlexWrap, Float, FontStyle, GridAutoFlow, GridPlacement, GridTrackSize, JustifyContent, + JustifyItems, JustifySelf, LengthOrAuto, Overflow, Position, StyledNode, TextAlign, + TextDecoration, TextOverflow, Transform, Visibility, WhiteSpace, WillChange, }; use we_text::font::Font; @@ -247,6 +247,10 @@ pub struct LayoutBox { pub visibility: Visibility, /// CSS `opacity` property (0.0–1.0). pub opacity: f32, + /// CSS `transform` property. + pub transform: Transform, + /// CSS `appearance` / `-webkit-appearance` property. + pub appearance: Appearance, /// CSS `will-change` property. pub will_change: WillChange, /// CSS `float` property. @@ -380,6 +384,8 @@ impl LayoutBox { sticky_constraint: None, visibility: style.visibility, opacity: style.opacity, + transform: style.transform, + appearance: style.appearance, will_change: style.will_change, float: style.float, clear: style.clear, diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 73ae0bd..5966f68 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -22,8 +22,8 @@ use we_layout::{ SCROLLBAR_WIDTH, }; use we_style::computed::{ - BackgroundRepeat, BackgroundSize, BorderStyle, Display, LengthOrAuto, Overflow, Position, - TextDecoration, Visibility, WillChange, + Appearance, BackgroundRepeat, BackgroundSize, BorderStyle, Display, LengthOrAuto, Overflow, + Position, TextDecoration, Visibility, WillChange, }; /// Scroll state: maps NodeId of scrollable boxes to their (scroll_x, scroll_y) offsets. @@ -301,6 +301,10 @@ fn paint_box( sticky_ref_screen_y: f32, dropdowns: &mut Vec, ) { + if layout_box.transform.paints_as_zero_scale() { + return; + } + let visible = layout_box.visibility == Visibility::Visible; let tx = translate.0; let ty = translate.1; @@ -1448,7 +1452,10 @@ fn paint_text_input( FC_TEXT_COLOR }; - if has_border_radius(layout_box) { + if layout_box.appearance == Appearance::None { + paint_background(layout_box, list, tx, ty); + paint_borders(layout_box, list, tx, ty); + } else if has_border_radius(layout_box) { // CSS-styled input (e.g. the browser chrome address bar): honor the // author's background and rounded border instead of the default inset // look. A transparent CSS background falls back to the field default. @@ -2411,6 +2418,37 @@ mod tests { } } + #[test] + fn transform_scale_zero_suppresses_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 has_fill = |color| { + list.iter().any(|cmd| { + matches!( + cmd, + PaintCommand::FillRect { color: fill, .. } if *fill == color + ) + }) + }; + + assert!(!has_fill(Color::rgb(255, 0, 0))); + assert!(!has_fill(Color::rgb(0, 0, 255))); + assert!(has_fill(Color::rgb(0, 128, 0))); + } + #[test] fn canvas_background_propagates_from_body() { // example.com pattern: `body { background:#eee }`, html transparent. @@ -3509,6 +3547,43 @@ body { margin: 0; } ); } + #[test] + fn appearance_none_text_input_skips_native_chrome() { + let html_str = r#" + +
"#; + let doc = we_html::parse_html(html_str); + let tree = layout_doc(&doc); + let list = build_display_list(&tree); + + assert!( + list.iter().any(|cmd| matches!( + cmd, + PaintCommand::RoundedRect { color, .. } if *color == Color::rgb(255, 255, 255) + )), + "rounded parent background should still paint" + ); + assert!( + !list.iter().any(|cmd| matches!( + cmd, + PaintCommand::FillRect { color, width, height, .. } + if *color == FC_BG_COLOR && *width >= 100.0 && *height >= 30.0 + )), + "appearance:none text input should not paint native white field chrome" + ); + } + #[test] fn empty_text_input_renders_placeholder() { let html_str = r#" diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index 47a08fc..a45e03d 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -153,6 +153,18 @@ pub enum Position { Sticky, } +// --------------------------------------------------------------------------- +// Appearance +// --------------------------------------------------------------------------- + +/// CSS `appearance` / `-webkit-appearance` values. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Appearance { + #[default] + Auto, + None, +} + // --------------------------------------------------------------------------- // FontWeight // --------------------------------------------------------------------------- @@ -317,6 +329,29 @@ pub struct WillChange { pub opacity: bool, } +// --------------------------------------------------------------------------- +// Transform +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum Transform { + #[default] + None, + Scale { + x: f32, + y: f32, + }, +} + +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, + } + } +} + // --------------------------------------------------------------------------- // Visibility // --------------------------------------------------------------------------- @@ -790,6 +825,12 @@ pub struct ComputedStyle { // Opacity (not inherited) pub opacity: f32, + // Transform (not inherited) + pub transform: Transform, + + // Native control appearance (not inherited) + pub appearance: Appearance, + // Will-change (not inherited) pub will_change: WillChange, @@ -920,6 +961,8 @@ impl Default for ComputedStyle { text_overflow: TextOverflow::default(), line_clamp: None, opacity: 1.0, + transform: Transform::None, + appearance: Appearance::Auto, will_change: WillChange::default(), visibility: Visibility::Visible, cursor: Cursor::Auto, @@ -1658,6 +1701,19 @@ fn resolve_color(value: &CssValue, current_color: Color) -> Option { } } +fn parse_transform(value: &CssValue) -> Option { + match value { + CssValue::None => Some(Transform::None), + CssValue::Keyword(k) if k.as_str() == "none" => Some(Transform::None), + CssValue::TransformScale(x, y) => Some(Transform::Scale { + x: *x as f32, + y: *y as f32, + }), + CssValue::List(values) => values.iter().find_map(parse_transform), + _ => None, + } +} + // --------------------------------------------------------------------------- // Apply a single property value to a ComputedStyle // --------------------------------------------------------------------------- @@ -2181,6 +2237,30 @@ 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" => { + if let Some(transform) = parse_transform(value) { + style.transform = transform; + } + } + + // Native control appearance. This is primarily used by form controls: + // author CSS such as `-webkit-appearance:none` should opt out of the + // renderer's fallback native chrome. + "appearance" | "-webkit-appearance" => { + style.appearance = match value { + CssValue::None => Appearance::None, + CssValue::Keyword(k) => match k.as_str() { + "none" => Appearance::None, + "auto" | "textfield" | "searchfield" => Appearance::Auto, + _ => style.appearance, + }, + _ => style.appearance, + }; + } + // Will-change "will-change" => { style.will_change = match value { @@ -2972,6 +3052,8 @@ fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &Computed "text-overflow" => style.text_overflow = parent.text_overflow, "-webkit-line-clamp" | "line-clamp" => style.line_clamp = parent.line_clamp, "opacity" => style.opacity = parent.opacity, + "transform" => style.transform = parent.transform, + "appearance" | "-webkit-appearance" => style.appearance = parent.appearance, "will-change" => style.will_change = parent.will_change, "flex-direction" => style.flex_direction = parent.flex_direction, "flex-wrap" => style.flex_wrap = parent.flex_wrap, @@ -3069,6 +3151,8 @@ fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { "text-overflow" => style.text_overflow = initial.text_overflow, "-webkit-line-clamp" | "line-clamp" => style.line_clamp = initial.line_clamp, "opacity" => style.opacity = initial.opacity, + "transform" => style.transform = initial.transform, + "appearance" | "-webkit-appearance" => style.appearance = initial.appearance, "will-change" => style.will_change = initial.will_change, "visibility" => style.visibility = initial.visibility, "cursor" => style.cursor = initial.cursor, @@ -4918,6 +5002,49 @@ mod tests { assert!((style_default.opacity - 1.0).abs() < 0.001); } + #[test] + fn transform_scale_parsing() { + let html_str = r#" + +
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 collapsed = &body.children[0]; + let wide = &body.children[1]; + + assert_eq!( + collapsed.style.transform, + Transform::Scale { x: 0.0, y: 0.0 } + ); + assert!(collapsed.style.transform.paints_as_zero_scale()); + assert_eq!(wide.style.transform, Transform::Scale { x: 1.0, y: 0.5 }); + assert!(!wide.style.transform.paints_as_zero_scale()); + } + + #[test] + fn appearance_none_parsing() { + let html_str = r#" + +"#; + 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 plain = &body.children[0]; + let native = &body.children[1]; + + assert_eq!(plain.style.appearance, Appearance::None); + assert_eq!(native.style.appearance, Appearance::Auto); + } + #[test] fn will_change_parsing() { let html_str = r#" -- 2.51.2