From 773b1415b55026244dcc87faf7c05a17a62d1992 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Thu, 16 Jul 2026 17:59:50 +0800 Subject: [PATCH] Add CSS linear-gradient background painting Improve Bing real-web parity by parsing/computing/painting CSS linear-gradient backgrounds and correcting Metal source-over alpha blending. Adds isu issue 381 for the remaining box-shadow primitive surfaced while working on isu issue 280; issue 280 remains open because its Chromium screenshot acceptance threshold is still blocked by isu issue 305 and other layout gaps. --- .isu/issues.json | 15 ++- crates/css/src/values.rs | 203 ++++++++++++++++++++++++++++++++++- crates/layout/src/lib.rs | 5 +- crates/platform/src/metal.rs | 12 ++- crates/render/src/gpu.rs | 116 +++++++++++++++++++- crates/render/src/lib.rs | 58 +++++++++- crates/style/src/computed.rs | 57 +++++++++- 7 files changed, 452 insertions(+), 14 deletions(-) diff --git a/.isu/issues.json b/.isu/issues.json index 2049825..54ef7ed 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 381, + "next_id": 382, "issues": [ { "id": 1, @@ -4660,6 +4660,19 @@ "author": "piefev", "state": "open", "created_at": "2026-07-16T07:05:31Z" + }, + { + "id": 381, + "repo": "we", + "title": "Implement CSS box-shadow painting for real-web parity", + "body": "Parent: isu issue 280.\\n\\nThe Bing real-web parity pass now parses and paints CSS linear-gradient backgrounds, and Metal source-over alpha preserves opaque output alpha, but the same-snapshot Chromium comparison still shows visible differences around controls/cards. One concrete missing primitive is CSS box-shadow: the engine has no box-shadow parsing/computed/layout/render support, while Bing uses box-shadow on controls such as overflow menus and buttons/search UI.\\n\\nRepro:\\n`cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/bing.com.we --out-dir crates/e2e/artifacts`\\n\\nCurrent useful comparison after the gradient/alpha work in isu issue 280:\\n- committed Bing Chromium goldens are still stale against the Sichuan Tea snapshot; see isu issue 305\\n- same-snapshot Chromium artifacts are available at `crates/e2e/artifacts/real-web/bing.com/desktop.scenario-cache.chromium.png` and `mobile.scenario-cache.chromium.png`\\n- current we output still lacks Chromium-like shadows around several Bing controls/cards\\n\\nExpected: implement non-inset CSS box-shadow parsing and painting, with tests, then re-run the Bing real-web scenario and compare against the same-snapshot Chromium artifacts. Keep isu issue 280 open until all remaining parity gaps and the stale-golden issue are resolved.", + "labels": [ + "real-web" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-16T09:55:15Z" } ] } diff --git a/crates/css/src/values.rs b/crates/css/src/values.rs index 5dd50e2..2531e1f 100644 --- a/crates/css/src/values.rs +++ b/crates/css/src/values.rs @@ -23,6 +23,8 @@ pub enum CssValue { String(String), /// A URL value from `url(...)`. Url(String), + /// A parsed `linear-gradient(...)` image value. + LinearGradient(LinearGradient), /// A keyword (ident). Keyword(String), /// The `auto` keyword. @@ -49,6 +51,34 @@ pub enum CssValue { TransformScale(f64, f64), } +/// Direction of a parsed `linear-gradient(...)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LinearGradientDirection { + ToBottom, + ToTop, + ToRight, + ToLeft, + ToBottomRight, + ToBottomLeft, + ToTopRight, + ToTopLeft, +} + +/// One color stop in a parsed `linear-gradient(...)`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct GradientStop { + pub color: Color, + /// Stop position as a normalized fraction of the gradient line. + pub position: Option, +} + +/// Minimal linear-gradient representation used by the renderer. +#[derive(Debug, Clone, PartialEq)] +pub struct LinearGradient { + pub direction: LinearGradientDirection, + pub stops: Vec, +} + // --------------------------------------------------------------------------- // CSS math expression AST (calc, min, max, clamp) // --------------------------------------------------------------------------- @@ -275,6 +305,7 @@ fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { "url" => parse_url(args), "rgb" => parse_rgb(args, false), "rgba" => parse_rgb(args, true), + "linear-gradient" => parse_linear_gradient(args), "hsl" => parse_hsl(args), "hsla" => parse_hsl(args), "calc" => parse_math_calc(args), @@ -286,6 +317,92 @@ fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { } } +fn parse_linear_gradient(args: &[ComponentValue]) -> CssValue { + let mut groups: Vec> = vec![Vec::new()]; + for arg in args { + if matches!(arg, ComponentValue::Comma) { + groups.push(Vec::new()); + } else { + groups.last_mut().unwrap().push(arg.clone()); + } + } + groups.retain(|group| { + group + .iter() + .any(|v| !matches!(v, ComponentValue::Whitespace)) + }); + + let mut direction = LinearGradientDirection::ToBottom; + if let Some(first) = groups.first() { + if let Some(parsed_direction) = parse_linear_gradient_direction(first) { + direction = parsed_direction; + groups.remove(0); + } + } + + let stops: Vec = groups + .iter() + .filter_map(|group| parse_gradient_stop(group)) + .collect(); + if stops.len() < 2 { + return CssValue::Keyword("linear-gradient()".to_string()); + } + + CssValue::LinearGradient(LinearGradient { direction, stops }) +} + +fn parse_linear_gradient_direction(group: &[ComponentValue]) -> Option { + let idents: Vec = group + .iter() + .filter_map(|cv| match cv { + ComponentValue::Ident(s) => Some(s.to_ascii_lowercase()), + ComponentValue::Whitespace => None, + _ => Some(String::new()), + }) + .collect(); + if idents.first().map(String::as_str) != Some("to") { + return None; + } + + let has_top = idents.iter().any(|s| s == "top"); + let has_right = idents.iter().any(|s| s == "right"); + let has_bottom = idents.iter().any(|s| s == "bottom"); + let has_left = idents.iter().any(|s| s == "left"); + match (has_top, has_right, has_bottom, has_left) { + (true, true, false, false) => Some(LinearGradientDirection::ToTopRight), + (true, false, false, true) => Some(LinearGradientDirection::ToTopLeft), + (false, true, true, false) => Some(LinearGradientDirection::ToBottomRight), + (false, false, true, true) => Some(LinearGradientDirection::ToBottomLeft), + (true, false, false, false) => Some(LinearGradientDirection::ToTop), + (false, true, false, false) => Some(LinearGradientDirection::ToRight), + (false, false, true, false) => Some(LinearGradientDirection::ToBottom), + (false, false, false, true) => Some(LinearGradientDirection::ToLeft), + _ => None, + } +} + +fn parse_gradient_stop(group: &[ComponentValue]) -> Option { + let non_ws: Vec<&ComponentValue> = group + .iter() + .filter(|cv| !matches!(cv, ComponentValue::Whitespace)) + .collect(); + let first = non_ws.first()?; + let color = match parse_single_value(first) { + CssValue::Color(c) => c, + CssValue::Transparent => Color::new(0, 0, 0, 0), + CssValue::CurrentColor => return None, + _ => return None, + }; + let position = non_ws.iter().skip(1).find_map(|cv| match cv { + ComponentValue::Percentage(p) => Some((*p as f32) / 100.0), + ComponentValue::Number(n, _) => Some(*n as f32), + ComponentValue::Dimension(n, _, unit) if unit.eq_ignore_ascii_case("px") => Some(*n as f32), + _ => None, + }); + + Some(GradientStop { color, position }) +} + fn parse_transform_scale(args: &[ComponentValue]) -> CssValue { let values: Vec = args .iter() @@ -1331,7 +1448,7 @@ fn expand_background(values: &[ComponentValue], important: bool) -> Vec { bg_color = val.clone() } - CssValue::Url(_) => bg_image = Some(val.clone()), + CssValue::Url(_) | CssValue::LinearGradient(_) => bg_image = Some(val.clone()), CssValue::Keyword(kw) => { if let Some(c) = named_color(kw) { bg_color = CssValue::Color(c); @@ -1939,6 +2056,90 @@ mod tests { assert_eq!(result[1].value, CssValue::Url("hero.jpg".to_string())); } + #[test] + fn test_parse_linear_gradient() { + let cv = ComponentValue::Function( + "linear-gradient".to_string(), + vec![ + ComponentValue::Ident("to".to_string()), + ComponentValue::Whitespace, + ComponentValue::Ident("bottom".to_string()), + ComponentValue::Whitespace, + ComponentValue::Ident("left".to_string()), + ComponentValue::Comma, + ComponentValue::Function( + "rgba".to_string(), + vec![ + ComponentValue::Number(0.0, NumericType::Integer), + ComponentValue::Comma, + ComponentValue::Number(0.0, NumericType::Integer), + ComponentValue::Comma, + ComponentValue::Number(0.0, NumericType::Integer), + ComponentValue::Comma, + ComponentValue::Number(0.6, NumericType::Number), + ], + ), + ComponentValue::Whitespace, + ComponentValue::Percentage(-20.0), + ComponentValue::Comma, + ComponentValue::Ident("transparent".to_string()), + ComponentValue::Whitespace, + ComponentValue::Percentage(30.0), + ], + ); + + assert_eq!( + parse_single_value(&cv), + CssValue::LinearGradient(LinearGradient { + direction: LinearGradientDirection::ToBottomLeft, + stops: vec![ + GradientStop { + color: Color::new(0, 0, 0, 153), + position: Some(-0.2), + }, + GradientStop { + color: Color::new(0, 0, 0, 0), + position: Some(0.3), + }, + ], + }) + ); + } + + #[test] + fn test_background_shorthand_gradient_image() { + let values = vec![ComponentValue::Function( + "linear-gradient".to_string(), + vec![ + ComponentValue::Function( + "rgba".to_string(), + vec![ + ComponentValue::Number(0.0, NumericType::Integer), + ComponentValue::Comma, + ComponentValue::Number(0.0, NumericType::Integer), + ComponentValue::Comma, + ComponentValue::Number(0.0, NumericType::Integer), + ComponentValue::Comma, + ComponentValue::Number(0.6, NumericType::Number), + ], + ), + ComponentValue::Comma, + ComponentValue::Ident("transparent".to_string()), + ], + )]; + let result = expand_shorthand("background", &values, false).unwrap(); + assert_eq!(result.len(), 2); + assert_eq!(result[0].property, "background-color"); + assert_eq!(result[1].property, "background-image"); + assert!(matches!( + result[1].value, + CssValue::LinearGradient(LinearGradient { + direction: LinearGradientDirection::ToBottom, + .. + }) + )); + } + #[test] fn test_non_shorthand_returns_none() { let values = vec![ComponentValue::Ident("red".to_string())]; diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 54933c5..b4109a9 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -6,7 +6,7 @@ use std::cell::Cell; use std::collections::{HashMap, HashSet}; -use we_css::values::Color; +use we_css::values::{Color, LinearGradient}; use we_dom::{Document, NodeData, NodeId}; use we_style::computed::{ AlignContent, AlignItems, AlignSelf, Appearance, BackgroundRepeat, BackgroundSize, @@ -187,6 +187,8 @@ pub struct LayoutBox { pub background_color: Color, /// Background image URL, if any. pub background_image: Option, + /// CSS linear-gradient background, if any. + pub background_gradient: Option, pub background_size: BackgroundSize, pub background_position_x: LengthOrAuto, pub background_position_y: LengthOrAuto, @@ -328,6 +330,7 @@ impl LayoutBox { color: style.color, background_color: style.background_color, background_image: style.background_image.as_ref().map(|url| url.to_string()), + background_gradient: style.background_gradient.clone(), background_size: style.background_size, background_position_x: style.background_position_x, background_position_y: style.background_position_y, diff --git a/crates/platform/src/metal.rs b/crates/platform/src/metal.rs index 5e5cca6..fea3f68 100644 --- a/crates/platform/src/metal.rs +++ b/crates/platform/src/metal.rs @@ -136,6 +136,9 @@ pub const MTL_PRIMITIVE_TYPE_TRIANGLE: u64 = 3; /// `MTLBlendFactorSourceAlpha` const MTL_BLEND_FACTOR_SOURCE_ALPHA: u64 = 4; +/// `MTLBlendFactorOne` +const MTL_BLEND_FACTOR_ONE: u64 = 1; + /// `MTLBlendFactorOneMinusSourceAlpha` const MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA: u64 = 5; @@ -1465,7 +1468,7 @@ pub fn drawable_texture(drawable: Id) -> Option { /// Create the 2D render pipeline state from compiled MSL shaders. /// -/// Configures alpha blending: `source * sourceAlpha + dest * (1 - sourceAlpha)`. +/// Configures source-over alpha blending. fn create_render_pipeline(device: &Device) -> Option { // Compile shader source let library = device.new_library_with_source(SHADER_SOURCE)?; @@ -1508,15 +1511,15 @@ fn create_render_pipeline(device: &Device) -> Option { // Enable alpha blending let _: *mut c_void = msg_send![attachment, setBlendingEnabled: true]; - // source * sourceAlpha + dest * (1 - sourceAlpha) + // RGB: source * sourceAlpha + dest * (1 - sourceAlpha) let _: *mut c_void = msg_send![attachment, setSourceRGBBlendFactor: MTL_BLEND_FACTOR_SOURCE_ALPHA]; let _: *mut c_void = msg_send![ attachment, setDestinationRGBBlendFactor: MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA ]; - let _: *mut c_void = - msg_send![attachment, setSourceAlphaBlendFactor: MTL_BLEND_FACTOR_SOURCE_ALPHA]; + // A: sourceAlpha + destAlpha * (1 - sourceAlpha) + let _: *mut c_void = msg_send![attachment, setSourceAlphaBlendFactor: MTL_BLEND_FACTOR_ONE]; let _: *mut c_void = msg_send![ attachment, setDestinationAlphaBlendFactor: MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA @@ -2136,6 +2139,7 @@ mod tests { #[test] fn blend_factor_constants() { + assert_eq!(MTL_BLEND_FACTOR_ONE, 1); assert_eq!(MTL_BLEND_FACTOR_SOURCE_ALPHA, 4); assert_eq!(MTL_BLEND_FACTOR_ONE_MINUS_SOURCE_ALPHA, 5); } diff --git a/crates/render/src/gpu.rs b/crates/render/src/gpu.rs index 023d21b..8fe10d2 100644 --- a/crates/render/src/gpu.rs +++ b/crates/render/src/gpu.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; -use we_css::values::Color; +use we_css::values::{Color, LinearGradient, LinearGradientDirection}; use we_dom::NodeId; use we_image::pixel::Image; use we_platform::metal::{ @@ -605,6 +605,29 @@ impl GpuRenderer { ); } + PaintCommand::LinearGradient { + x, + y, + width, + height, + gradient, + } => { + let tex_key = TextureKey::Solid; + if tex_key != batch_texture && !batch_vertices.is_empty() { + self.flush_batch(&batch_vertices, batch_texture, &encoder); + batch_vertices.clear(); + } + batch_texture = tex_key; + push_linear_gradient_quad( + &mut batch_vertices, + *x + coord_offset.0, + *y + coord_offset.1, + *width, + *height, + gradient, + ); + } + PaintCommand::RoundedRect { x, y, @@ -1022,6 +1045,97 @@ fn push_solid_quad(out: &mut Vec, x: f32, y: f32, w: f32, h: f32, color: }); } +/// Push 6 vertices for a CSS linear-gradient rectangle. +fn push_linear_gradient_quad( + out: &mut Vec, + x: f32, + y: f32, + w: f32, + h: f32, + gradient: &LinearGradient, +) { + let x0 = x; + let y0 = y; + let x1 = x + w; + let y1 = y + h; + let tl = color_to_f32(&gradient_color_at( + gradient, + gradient_t(gradient.direction, 0.0, 0.0), + )); + let tr = color_to_f32(&gradient_color_at( + gradient, + gradient_t(gradient.direction, 1.0, 0.0), + )); + let br = color_to_f32(&gradient_color_at( + gradient, + gradient_t(gradient.direction, 1.0, 1.0), + )); + let bl = color_to_f32(&gradient_color_at( + gradient, + gradient_t(gradient.direction, 0.0, 1.0), + )); + + let vert = |px: f32, py: f32, color: [f32; 4]| Vertex { + position: [px, py], + color, + tex_coord: [0.0, 0.0], + use_texture: TEXTURE_MODE_SOLID, + ..Vertex::default() + }; + + out.push(vert(x0, y0, tl)); + out.push(vert(x1, y0, tr)); + out.push(vert(x0, y1, bl)); + out.push(vert(x1, y0, tr)); + out.push(vert(x1, y1, br)); + out.push(vert(x0, y1, bl)); +} + +fn gradient_t(direction: LinearGradientDirection, x: f32, y: f32) -> f32 { + match direction { + LinearGradientDirection::ToBottom => y, + LinearGradientDirection::ToTop => 1.0 - y, + LinearGradientDirection::ToRight => x, + LinearGradientDirection::ToLeft => 1.0 - x, + LinearGradientDirection::ToBottomRight => (x + y) * 0.5, + LinearGradientDirection::ToBottomLeft => ((1.0 - x) + y) * 0.5, + LinearGradientDirection::ToTopRight => (x + (1.0 - y)) * 0.5, + LinearGradientDirection::ToTopLeft => ((1.0 - x) + (1.0 - y)) * 0.5, + } +} + +fn gradient_color_at(gradient: &LinearGradient, t: f32) -> Color { + let Some(first) = gradient.stops.first() else { + return Color::new(0, 0, 0, 0); + }; + let Some(last) = gradient.stops.last() else { + return first.color; + }; + let first_pos = first.position.unwrap_or(0.0); + let last_pos = last.position.unwrap_or(1.0); + let span = last_pos - first_pos; + if span.abs() <= f32::EPSILON { + return last.color; + } + let local = ((t - first_pos) / span).clamp(0.0, 1.0); + lerp_color(first.color, last.color, local) +} + +fn lerp_color(a: Color, b: Color, t: f32) -> Color { + fn channel(a: u8, b: u8, t: f32) -> u8 { + (a as f32 + (b as f32 - a as f32) * t) + .round() + .clamp(0.0, 255.0) as u8 + } + + Color::new( + channel(a.r, b.r, t), + channel(a.g, b.g, t), + channel(a.b, b.b, t), + channel(a.a, b.a, t), + ) +} + /// Push 6 vertices (2 triangles) covering a rounded rectangle's bounding box, /// tagged for the fragment shader's signed-distance-field rounded-rect mode. /// `radii` is (top-left, top-right, bottom-right, bottom-left). `stroke_width` diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index ba8c4bd..dd77c4c 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -15,7 +15,7 @@ pub use gpu::GpuRenderer; use std::collections::HashMap; -use we_css::values::Color; +use we_css::values::{Color, LinearGradient}; use we_dom::NodeId; use we_layout::{ BoxType, FormControlInfo, FormControlType, LayoutBox, LayoutTree, Rect, SelectOption, TextLine, @@ -70,6 +70,14 @@ pub enum PaintCommand { stroke_width: f32, color: Color, }, + /// Fill a rectangle with a CSS linear gradient. + LinearGradient { + x: f32, + y: f32, + width: f32, + height: f32, + gradient: LinearGradient, + }, /// Draw a text fragment at a position with styling. DrawGlyphs { line: TextLine, @@ -824,6 +832,15 @@ fn paint_background(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty: color: bg, }); } + if let Some(gradient) = &layout_box.background_gradient { + list.push(PaintCommand::LinearGradient { + x: bb.x + tx, + y: bb.y + ty, + width: bb.width, + height: bb.height, + gradient: gradient.clone(), + }); + } if layout_box.background_image.is_some() { if let Some(node_id) = node_id_from_box_type(&layout_box.box_type) { list.push(PaintCommand::DrawBackgroundImage { @@ -2658,6 +2675,45 @@ body { margin: 0; } ); } + #[test] + fn css_linear_gradient_emits_gradient_paint_command() { + let html_str = r#" + + +
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = we_layout::layout( + &styled, + &doc, + 800.0, + 600.0, + &font, + &std::collections::HashMap::new(), + ); + + let list = build_display_list(&tree); + let gradient = list + .iter() + .find_map(|c| match c { + PaintCommand::LinearGradient { + width, + height, + gradient, + .. + } => Some((*width, *height, gradient)), + _ => None, + }) + .expect("linear-gradient should produce a gradient paint command"); + assert!((gradient.0 - 100.0).abs() < 0.5); + assert!((gradient.1 - 50.0).abs() < 0.5); + assert_eq!(gradient.2.stops[0].color, Color::new(0, 0, 0, 153)); + assert_eq!(gradient.2.stops[1].color, Color::new(0, 0, 0, 0)); + } + #[test] fn hsl_flex_descendant_backgrounds_render_at_flex_positions() { let html_str = r#" diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index 3e29235..9b31f76 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -19,7 +19,9 @@ use we_css::transitions::{ parse_transition_delay, parse_transition_duration, parse_transition_property, parse_transition_shorthand, parse_transition_timing_function, SingleTransition, TransitionSpec, }; -use we_css::values::{expand_shorthand, parse_value, Color, CssValue, LengthUnit, MathExpr}; +use we_css::values::{ + expand_shorthand, parse_value, Color, CssValue, LengthUnit, LinearGradient, MathExpr, +}; use we_dom::{Document, NodeData, NodeId}; use we_memory::intern::Atom; @@ -796,6 +798,7 @@ pub struct ComputedStyle { // Background pub background_color: Color, pub background_image: Option, + pub background_gradient: Option, pub background_size: BackgroundSize, pub background_position_x: LengthOrAuto, pub background_position_y: LengthOrAuto, @@ -944,6 +947,7 @@ impl Default for ComputedStyle { background_color: Color::new(0, 0, 0, 0), // transparent background_image: None, + background_gradient: None, background_size: BackgroundSize::Auto, background_position_x: LengthOrAuto::Percentage(0.0), background_position_y: LengthOrAuto::Percentage(0.0), @@ -2121,8 +2125,18 @@ fn apply_property( } } "background-image" => match value { - CssValue::Url(url) => style.background_image = Some(Atom::new(url)), - CssValue::None => style.background_image = None, + CssValue::Url(url) => { + style.background_image = Some(Atom::new(url)); + style.background_gradient = None; + } + CssValue::LinearGradient(gradient) => { + style.background_image = None; + style.background_gradient = Some(gradient.clone()); + } + CssValue::None => { + style.background_image = None; + style.background_gradient = None; + } _ => {} }, "background-size" => { @@ -3041,7 +3055,10 @@ fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &Computed "max-height" => style.max_height = parent.max_height, "box-sizing" => style.box_sizing = parent.box_sizing, "background-color" => style.background_color = parent.background_color, - "background-image" => style.background_image = parent.background_image.clone(), + "background-image" => { + style.background_image = parent.background_image.clone(); + style.background_gradient = parent.background_gradient.clone(); + } "background-size" => style.background_size = parent.background_size, "background-position" => { style.background_position_x = parent.background_position_x; @@ -3136,7 +3153,10 @@ fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { "line-height" => style.line_height = initial.line_height, "white-space" => style.white_space = initial.white_space, "background-color" => style.background_color = initial.background_color, - "background-image" => style.background_image = initial.background_image.clone(), + "background-image" => { + style.background_image = initial.background_image.clone(); + style.background_gradient = initial.background_gradient.clone(); + } "background-size" => style.background_size = initial.background_size, "background-position" => { style.background_position_x = initial.background_position_x; @@ -4132,6 +4152,33 @@ mod tests { assert_eq!(div_node.style.background_repeat, BackgroundRepeat::NoRepeat); } + #[test] + fn author_background_linear_gradient() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + doc.append_child(body, div); + + let ss = Parser::parse( + "div { background: linear-gradient(to bottom, rgba(0,0,0,.6), transparent); }", + ); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let gradient = div_node + .style + .background_gradient + .as_ref() + .expect("gradient background should compute"); + assert_eq!( + gradient.direction, + we_css::values::LinearGradientDirection::ToBottom + ); + assert_eq!(gradient.stops.len(), 2); + assert_eq!(gradient.stops[0].color, Color::new(0, 0, 0, 153)); + assert_eq!(gradient.stops[1].color, Color::new(0, 0, 0, 0)); + assert!(div_node.style.background_image.is_none()); + } + #[test] fn author_font_size_px() { let (mut doc, _, _, body) = make_doc_with_body(); -- 2.51.2