From e62ad7e68017d31309aa664abb7427944c92f654 Mon Sep 17 00:00:00 2001 From: Pyfisch Date: Sat, 29 Apr 2017 20:00:13 +0200 Subject: [PATCH 1/7] Implement -webkit-radial-gradient aliases. In display_list_builder.rs handle cover and contain size keywords. --- components/layout/display_list_builder.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index a2453067e1f..801f739fc7a 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -710,7 +710,7 @@ fn convert_circle_size_keyword(keyword: SizeKeyword, center: &Point2D) -> Size2D { use style::values::computed::image::SizeKeyword::*; let radius = match keyword { - ClosestSide => { + ClosestSide | Contain => { let dist = get_distance_to_sides(size, center, ::std::cmp::min); ::std::cmp::min(dist.width, dist.height) } @@ -719,12 +719,7 @@ fn convert_circle_size_keyword(keyword: SizeKeyword, ::std::cmp::max(dist.width, dist.height) } ClosestCorner => get_distance_to_corner(size, center, ::std::cmp::min), - FarthestCorner => get_distance_to_corner(size, center, ::std::cmp::max), - _ => { - // TODO(#16542) - println!("TODO: implement size keyword {:?} for circles", keyword); - Au::new(0) - } + FarthestCorner | Cover => get_distance_to_corner(size, center, ::std::cmp::max), }; Size2D::new(radius, radius) } @@ -736,15 +731,10 @@ fn convert_ellipse_size_keyword(keyword: SizeKeyword, center: &Point2D) -> Size2D { use style::values::computed::image::SizeKeyword::*; match keyword { - ClosestSide => get_distance_to_sides(size, center, ::std::cmp::min), + ClosestSide | Contain => get_distance_to_sides(size, center, ::std::cmp::min), FarthestSide => get_distance_to_sides(size, center, ::std::cmp::max), ClosestCorner => get_ellipse_radius(size, center, ::std::cmp::min), - FarthestCorner => get_ellipse_radius(size, center, ::std::cmp::max), - _ => { - // TODO(#16542) - println!("TODO: implement size keyword {:?} for ellipses", keyword); - Size2D::new(Au::new(0), Au::new(0)) - } + FarthestCorner | Cover => get_ellipse_radius(size, center, ::std::cmp::max), } } From a956e3fd529715cc0ac39b23910f19e092c7c5a9 Mon Sep 17 00:00:00 2001 From: Pyfisch Date: Sun, 30 Apr 2017 18:57:26 +0200 Subject: [PATCH 2/7] Improve convert_gradient_stops function. Implement Specification from CSS Images 3. Improvements: 1. Stops with positions less than preceding stops are moved. A common pattern is blue `70%, white 0` for sharp transitions. 2. Stop runs are correct if first stop has a position. The list `black 0%, red, gold` now renders the same as `black, red, gold`. Other runs may also be corrected. 3. Absolute length are no longer capped to 100%. --- components/layout/display_list_builder.rs | 93 ++++++++++++++--------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index 801f739fc7a..ac3fb646b20 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -608,12 +608,48 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], // Determine the position of each stop per CSS-IMAGES § 3.4. // // FIXME(#3908, pcwalton): Make sure later stops can't be behind earlier stops. - let stop_items = gradient_items.iter().filter_map(|item| { + + // Only keep the color stops. Discard the color items. + let mut stop_items = gradient_items.iter().filter_map(|item| { match *item { - GradientItem::ColorStop(ref stop) => Some(stop), + GradientItem::ColorStop(ref stop) => Some(*stop), _ => None, } }).collect::>(); + + assert!(stop_items.len() >= 2); + + // Run the algorithm from + // https://drafts.csswg.org/css-images-3/#color-stop-syntax + + // Step 1: If nothing else is specified the first stop is + // at 0% and the last stop is at 100%. + { + let first = stop_items.first_mut().unwrap(); + if first.position.is_none() { + first.position = Some(LengthOrPercentage::Percentage(0.0)); + } + } + { + let last = stop_items.last_mut().unwrap(); + if last.position.is_none() { + last.position = Some(LengthOrPercentage::Percentage(1.0)); + } + } + + // Step 2: Move any stops placed before earlier stops to the + // same position as the preceding stop. + let mut state = stop_items.first().unwrap().position.unwrap(); + for stop in stop_items.iter_mut() { + if let Some(pos) = stop.position { + if position_to_offset(state, length) > position_to_offset(pos, length) { + stop.position = Some(state); + } + state = stop.position.unwrap(); + } + } + + // Step 3: Evenly space stops without position. let mut stops = Vec::with_capacity(stop_items.len()); let mut stop_run = None; for (i, stop) in stop_items.iter().enumerate() { @@ -621,44 +657,31 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], None => { if stop_run.is_none() { // Initialize a new stop run. - let start_offset = if i == 0 { - 0.0 - } else { - // `unwrap()` here should never fail because this is the beginning of - // a stop run, which is always bounded by a length or percentage. - position_to_offset(stop_items[i - 1].position.unwrap(), length) - }; - let (end_index, end_offset) = - match stop_items[i..] - .iter() - .enumerate() - .find(|&(_, ref stop)| stop.position.is_some()) { - None => (stop_items.len() - 1, 1.0), - Some((end_index, end_stop)) => { - // `unwrap()` here should never fail because this is the end of - // a stop run, which is always bounded by a length or - // percentage. - (end_index, - position_to_offset(end_stop.position.unwrap(), length)) - } - }; + // `unwrap()` here should never fail because this is the beginning of + // a stop run, which is always bounded by a length or percentage. + let start_offset = + position_to_offset(stop_items[i - 1].position.unwrap(), length); + // `unwrap()` here should never fail because this is the end of + // a stop run, which is always bounded by a length or percentage. + let (end_index, end_stop) = stop_items[(i + 1)..] + .iter() + .enumerate() + .find(|&(_, ref stop)| stop.position.is_some()) + .unwrap(); + let end_offset = position_to_offset(end_stop.position.unwrap(), length); stop_run = Some(StopRun { start_offset: start_offset, end_offset: end_offset, - start_index: i, - stop_count: end_index - i, + start_index: i - 1, + stop_count: end_index, }) } let stop_run = stop_run.unwrap(); let stop_run_length = stop_run.end_offset - stop_run.start_offset; - if stop_run.stop_count == 0 { - stop_run.end_offset - } else { - stop_run.start_offset + - stop_run_length * (i - stop_run.start_index) as f32 / - (stop_run.stop_count as f32) - } + stop_run.start_offset + + stop_run_length * (i - stop_run.start_index) as f32 / + ((2 + stop_run.stop_count) as f32) } Some(position) => { stop_run = None; @@ -2692,12 +2715,10 @@ struct StopRun { fn position_to_offset(position: LengthOrPercentage, Au(total_length): Au) -> f32 { match position { - LengthOrPercentage::Length(Au(length)) => { - (1.0f32).min(length as f32 / total_length as f32) - } + LengthOrPercentage::Length(Au(length)) => length as f32 / total_length as f32, LengthOrPercentage::Percentage(percentage) => percentage as f32, LengthOrPercentage::Calc(calc) => - (1.0f32).min(calc.percentage() + (calc.length().0 as f32) / (total_length as f32)), + calc.percentage() + (calc.length().0 as f32) / (total_length as f32), } } From b230be8aaf318fb754cf58e5cd243087df2f7e0f Mon Sep 17 00:00:00 2001 From: Pyfisch Date: Sun, 30 Apr 2017 22:36:45 +0200 Subject: [PATCH 3/7] Implement radial gradients for borders. The property border-image-outset is not yet implemented. Note: Also support repeating-linear-gradients for borders. --- components/gfx/display_list/mod.rs | 11 +++++++++++ components/layout/display_list_builder.rs | 20 ++++++++++++++++++-- components/layout/webrender_helpers.rs | 22 +++++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/components/gfx/display_list/mod.rs b/components/gfx/display_list/mod.rs index 77a64553c29..67979f75766 100644 --- a/components/gfx/display_list/mod.rs +++ b/components/gfx/display_list/mod.rs @@ -976,12 +976,23 @@ pub struct GradientBorder { pub outset: SideOffsets2D, } +/// A border that is made of radial gradient +#[derive(Clone, HeapSizeOf, Deserialize, Serialize)] +pub struct RadialGradientBorder { + /// The gradient info that this border uses, border-image-source. + pub gradient: RadialGradient, + + /// Outsets for the border, as per border-image-outset. + pub outset: SideOffsets2D, +} + /// Specifies the type of border #[derive(Clone, HeapSizeOf, Deserialize, Serialize)] pub enum BorderDetails { Normal(NormalBorder), Image(ImageBorder), Gradient(GradientBorder), + RadialGradient(RadialGradientBorder), } /// Paints a border. diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index ac3fb646b20..a91a1ce48da 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -1334,8 +1334,24 @@ impl FragmentDisplayListBuilding for Fragment { }), })); } - GradientKind::Radial(_, _) => { - // TODO(#16638): Handle border-image with radial gradient. + GradientKind::Radial(ref shape, ref center) => { + let grad = self.convert_radial_gradient(&bounds, + &gradient.items[..], + shape, + center, + gradient.repeating, + style); + state.add_display_item(DisplayItem::Border(box BorderDisplayItem { + base: base, + border_widths: border.to_physical(style.writing_mode), + details: BorderDetails::RadialGradient( + display_list::RadialGradientBorder { + gradient: grad, + + // TODO(gw): Support border-image-outset + outset: SideOffsets2D::zero(), + }), + })); } } } diff --git a/components/layout/webrender_helpers.rs b/components/layout/webrender_helpers.rs index 70479d43837..d498bc6067f 100644 --- a/components/layout/webrender_helpers.rs +++ b/components/layout/webrender_helpers.rs @@ -352,15 +352,35 @@ impl WebRenderDisplayItemConverter for DisplayItem { } } BorderDetails::Gradient(ref gradient) => { + let extend_mode = if gradient.gradient.repeating { + ExtendMode::Repeat + } else { + ExtendMode::Clamp + }; webrender_traits::BorderDetails::Gradient(webrender_traits::GradientBorder { gradient: builder.create_gradient( gradient.gradient.start_point.to_pointf(), gradient.gradient.end_point.to_pointf(), gradient.gradient.stops.clone(), - ExtendMode::Clamp), + extend_mode), outset: gradient.outset, }) } + BorderDetails::RadialGradient(ref gradient) => { + let extend_mode = if gradient.gradient.repeating { + ExtendMode::Repeat + } else { + ExtendMode::Clamp + }; + webrender_traits::BorderDetails::RadialGradient(webrender_traits::RadialGradientBorder { + gradient: builder.create_radial_gradient( + gradient.gradient.center.to_pointf(), + gradient.gradient.radius.to_sizef(), + gradient.gradient.stops.clone(), + extend_mode), + outset: gradient.outset, + }) + } }; builder.push_border(rect, clip, widths, details); From f7f077c3347886efc68f11609ac50d0e11081724 Mon Sep 17 00:00:00 2001 From: Pyfisch Date: Mon, 1 May 2017 13:22:39 +0200 Subject: [PATCH 4/7] Add fix_gradient_stops function. Render gradients where the last stop is the same position as the previous one like Chrome and Firefox do. --- components/layout/display_list_builder.rs | 34 +++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index a91a1ce48da..edf3775d338 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -650,7 +650,8 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], } // Step 3: Evenly space stops without position. - let mut stops = Vec::with_capacity(stop_items.len()); + // Note: Remove the + 1 if fix_gradient_stops is changed. + let mut stops = Vec::with_capacity(stop_items.len() + 1); let mut stop_run = None; for (i, stop) in stop_items.iter().enumerate() { let offset = match stop.position { @@ -696,6 +697,25 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], stops } +#[inline] +/// Duplicate the last stop if its position is smaller 100%. +/// +/// Explanation by pyfisch: +/// If the last stop is at the same position as the previous stop the +/// last color is ignored by webrender. This differs from the spec +/// (I think so). The implementations of Chrome and Firefox seem +/// to have the same problem but work fine if the position of the last +/// stop is smaller than 100%. (Otherwise they ignore the last stop.) +fn fix_gradient_stops(stops: &mut Vec) { + if stops.last().unwrap().offset < 1.0 { + let color = stops.last().unwrap().color; + stops.push(GradientStop { + offset: 1.0, + color: color, + }) + } +} + /// Returns the the distance to the nearest or farthest corner depending on the comperator. fn get_distance_to_corner(size: &Size2D, center: &Point2D, cmp: F) -> Au where F: Fn(Au, Au) -> Au @@ -1110,7 +1130,10 @@ impl FragmentDisplayListBuilding for Fragment { let length = Au::from_f32_px( (delta.x.to_f32_px() * 2.0).hypot(delta.y.to_f32_px() * 2.0)); - let stops = convert_gradient_stops(stops, length, style); + let mut stops = convert_gradient_stops(stops, length, style); + if !repeating { + fix_gradient_stops(&mut stops); + } let center = Point2D::new(bounds.size.width / 2, bounds.size.height / 2); @@ -1145,7 +1168,12 @@ impl FragmentDisplayListBuilding for Fragment { => convert_ellipse_size_keyword(word, &bounds.size, ¢er), }; let length = Au::from_f32_px(radius.width.to_f32_px().hypot(radius.height.to_f32_px())); - let stops = convert_gradient_stops(stops, length, style); + + let mut stops = convert_gradient_stops(stops, length, style); + if !repeating { + fix_gradient_stops(&mut stops); + } + display_list::RadialGradient { center: center, radius: radius, From 4f17b17082a1a72c8f9470f22f8018f1cb0e41ab Mon Sep 17 00:00:00 2001 From: Pyfisch Date: Mon, 1 May 2017 14:19:35 +0200 Subject: [PATCH 5/7] Correct virtual gradient ray length of radial gradients. The length should be the horizontal radius and not the hypotenuse. --- components/layout/display_list_builder.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index edf3775d338..4b358944d33 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -1167,9 +1167,8 @@ impl FragmentDisplayListBuilding for Fragment { EndingShape::Ellipse(LengthOrPercentageOrKeyword::Keyword(word)) => convert_ellipse_size_keyword(word, &bounds.size, ¢er), }; - let length = Au::from_f32_px(radius.width.to_f32_px().hypot(radius.height.to_f32_px())); - let mut stops = convert_gradient_stops(stops, length, style); + let mut stops = convert_gradient_stops(stops, radius.width, style); if !repeating { fix_gradient_stops(&mut stops); } From 72db8d85553c3d760ceed382f3a63e7287d042f7 Mon Sep 17 00:00:00 2001 From: Pyfisch Date: Sat, 6 May 2017 21:43:22 +0200 Subject: [PATCH 6/7] Duplicate first gradient stop if necessary. If the first stop of a non-repeating gradient is not placed at offset 0.0 it is duplicated at this position. This solves the problem of the first stop being ignored if it is placed at the same offset as the next stop. --- components/layout/display_list_builder.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index 4b358944d33..bf20fd4cae1 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -650,8 +650,8 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], } // Step 3: Evenly space stops without position. - // Note: Remove the + 1 if fix_gradient_stops is changed. - let mut stops = Vec::with_capacity(stop_items.len() + 1); + // Note: Remove the + 2 if fix_gradient_stops is changed. + let mut stops = Vec::with_capacity(stop_items.len() + 2); let mut stop_run = None; for (i, stop) in stop_items.iter().enumerate() { let offset = match stop.position { @@ -698,7 +698,7 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], } #[inline] -/// Duplicate the last stop if its position is smaller 100%. +/// Duplicate the first and last stops if necessary. /// /// Explanation by pyfisch: /// If the last stop is at the same position as the previous stop the @@ -706,7 +706,17 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], /// (I think so). The implementations of Chrome and Firefox seem /// to have the same problem but work fine if the position of the last /// stop is smaller than 100%. (Otherwise they ignore the last stop.) +/// +/// Similarly the first stop is duplicated if it is not placed +/// at the start of the virtual gradient ray. fn fix_gradient_stops(stops: &mut Vec) { + if stops.first().unwrap().offset > 0.0 { + let color = stops.first().unwrap().color; + stops.insert(0, GradientStop { + offset: 0.0, + color: color, + }) + } if stops.last().unwrap().offset < 1.0 { let color = stops.last().unwrap().color; stops.push(GradientStop { From f4fadc78753dc2c7886b6ae1709431ee4488d058 Mon Sep 17 00:00:00 2001 From: Pyfisch Date: Sun, 7 May 2017 12:28:57 +0200 Subject: [PATCH 7/7] Address feedback by emilio. --- components/layout/display_list_builder.rs | 34 +++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index bf20fd4cae1..cce4be4c7e3 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -603,13 +603,11 @@ fn build_border_radius_for_inner_rect(outer_rect: &Rect, } fn convert_gradient_stops(gradient_items: &[GradientItem], - length: Au, + total_length: Au, style: &ServoComputedValues) -> Vec { // Determine the position of each stop per CSS-IMAGES § 3.4. - // - // FIXME(#3908, pcwalton): Make sure later stops can't be behind earlier stops. - // Only keep the color stops. Discard the color items. + // Only keep the color stops, discard the color interpolation hints. let mut stop_items = gradient_items.iter().filter_map(|item| { match *item { GradientItem::ColorStop(ref stop) => Some(*stop), @@ -622,14 +620,15 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], // Run the algorithm from // https://drafts.csswg.org/css-images-3/#color-stop-syntax - // Step 1: If nothing else is specified the first stop is - // at 0% and the last stop is at 100%. + // Step 1: + // If the first color stop does not have a position, set its position to 0%. { let first = stop_items.first_mut().unwrap(); if first.position.is_none() { first.position = Some(LengthOrPercentage::Percentage(0.0)); } } + // If the last color stop does not have a position, set its position to 100%. { let last = stop_items.last_mut().unwrap(); if last.position.is_none() { @@ -639,13 +638,14 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], // Step 2: Move any stops placed before earlier stops to the // same position as the preceding stop. - let mut state = stop_items.first().unwrap().position.unwrap(); - for stop in stop_items.iter_mut() { + let mut last_stop_position = stop_items.first().unwrap().position.unwrap(); + for stop in stop_items.iter_mut().skip(1) { if let Some(pos) = stop.position { - if position_to_offset(state, length) > position_to_offset(pos, length) { - stop.position = Some(state); + if position_to_offset(last_stop_position, total_length) + > position_to_offset(pos, total_length) { + stop.position = Some(last_stop_position); } - state = stop.position.unwrap(); + last_stop_position = stop.position.unwrap(); } } @@ -661,7 +661,7 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], // `unwrap()` here should never fail because this is the beginning of // a stop run, which is always bounded by a length or percentage. let start_offset = - position_to_offset(stop_items[i - 1].position.unwrap(), length); + position_to_offset(stop_items[i - 1].position.unwrap(), total_length); // `unwrap()` here should never fail because this is the end of // a stop run, which is always bounded by a length or percentage. let (end_index, end_stop) = stop_items[(i + 1)..] @@ -669,7 +669,7 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], .enumerate() .find(|&(_, ref stop)| stop.position.is_some()) .unwrap(); - let end_offset = position_to_offset(end_stop.position.unwrap(), length); + let end_offset = position_to_offset(end_stop.position.unwrap(), total_length); stop_run = Some(StopRun { start_offset: start_offset, end_offset: end_offset, @@ -686,7 +686,7 @@ fn convert_gradient_stops(gradient_items: &[GradientItem], } Some(position) => { stop_run = None; - position_to_offset(position, length) + position_to_offset(position, total_length) } }; stops.push(GradientStop { @@ -1141,6 +1141,10 @@ impl FragmentDisplayListBuilding for Fragment { (delta.x.to_f32_px() * 2.0).hypot(delta.y.to_f32_px() * 2.0)); let mut stops = convert_gradient_stops(stops, length, style); + + // Only clamped gradients need to be fixed because in repeating gradients + // there is no "first" or "last" stop because they repeat infinitly in + // both directions, so the rendering is always correct. if !repeating { fix_gradient_stops(&mut stops); } @@ -1179,6 +1183,8 @@ impl FragmentDisplayListBuilding for Fragment { }; let mut stops = convert_gradient_stops(stops, radius.width, style); + // Repeating gradients have no last stops that can be ignored. So + // fixup is not necessary but may actually break the gradient. if !repeating { fix_gradient_stops(&mut stops); }