mirror of
https://github.com/servo/servo.git
synced 2025-06-14 11:24:33 +00:00
Unify background placement code
Merges the implementations for background-image placement from gradients and images. Add missing parts and fix bugs. Now supported are the CSS properties: * background-attachment (except for local value) * background-clip * background-origin * background-position-x/y * background-repeat * background-size It should be noted that backgrounds are not clipped to rounded border corners.
This commit is contained in:
parent
d96fb89c31
commit
3b3d4a9853
9 changed files with 306 additions and 344 deletions
|
@ -729,6 +729,7 @@ pub struct GradientDisplayItem {
|
||||||
///
|
///
|
||||||
/// Without tiles, the tile will be the same size as the background.
|
/// Without tiles, the tile will be the same size as the background.
|
||||||
pub tile: Size2D<Au>,
|
pub tile: Size2D<Au>,
|
||||||
|
pub tile_spacing: Size2D<Au>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paints a radial gradient.
|
/// Paints a radial gradient.
|
||||||
|
@ -763,6 +764,7 @@ pub struct RadialGradientDisplayItem {
|
||||||
///
|
///
|
||||||
/// Without tiles, the tile will be the same size as the background.
|
/// Without tiles, the tile will be the same size as the background.
|
||||||
pub tile: Size2D<Au>,
|
pub tile: Size2D<Au>,
|
||||||
|
pub tile_spacing: Size2D<Au>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A normal border, supporting CSS border styles.
|
/// A normal border, supporting CSS border styles.
|
||||||
|
|
|
@ -19,7 +19,7 @@ use flex::FlexFlow;
|
||||||
use flow::{BaseFlow, Flow, FlowFlags};
|
use flow::{BaseFlow, Flow, FlowFlags};
|
||||||
use flow_ref::FlowRef;
|
use flow_ref::FlowRef;
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use fragment::{CanvasFragmentSource, CoordinateSystem, Fragment, ImageFragmentInfo, ScannedTextFragmentInfo};
|
use fragment::{CanvasFragmentSource, CoordinateSystem, Fragment, ScannedTextFragmentInfo};
|
||||||
use fragment::SpecificFragmentInfo;
|
use fragment::SpecificFragmentInfo;
|
||||||
use gfx::display_list;
|
use gfx::display_list;
|
||||||
use gfx::display_list::{BLUR_INFLATION_FACTOR, BaseDisplayItem, BorderDetails, BorderDisplayItem};
|
use gfx::display_list::{BLUR_INFLATION_FACTOR, BaseDisplayItem, BorderDetails, BorderDisplayItem};
|
||||||
|
@ -48,7 +48,9 @@ use std::{cmp, f32};
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use style::computed_values::{background_attachment, background_clip, background_origin};
|
use style::computed_values::background_attachment::single_value::T as BackgroundAttachment;
|
||||||
|
use style::computed_values::background_clip::single_value::T as BackgroundClip;
|
||||||
|
use style::computed_values::background_origin::single_value::T as BackgroundOrigin;
|
||||||
use style::computed_values::border_style::T as BorderStyle;
|
use style::computed_values::border_style::T as BorderStyle;
|
||||||
use style::computed_values::cursor;
|
use style::computed_values::cursor;
|
||||||
use style::computed_values::image_rendering::T as ImageRendering;
|
use style::computed_values::image_rendering::T as ImageRendering;
|
||||||
|
@ -58,7 +60,6 @@ use style::computed_values::position::T as StylePosition;
|
||||||
use style::computed_values::visibility::T as Visibility;
|
use style::computed_values::visibility::T as Visibility;
|
||||||
use style::logical_geometry::{LogicalMargin, LogicalPoint, LogicalRect, LogicalSize, WritingMode};
|
use style::logical_geometry::{LogicalMargin, LogicalPoint, LogicalRect, LogicalSize, WritingMode};
|
||||||
use style::properties::ComputedValues;
|
use style::properties::ComputedValues;
|
||||||
use style::properties::longhands::background_origin::single_value::computed_value::T as BackgroundOrigin;
|
|
||||||
use style::properties::longhands::border_image_repeat::computed_value::RepeatKeyword;
|
use style::properties::longhands::border_image_repeat::computed_value::RepeatKeyword;
|
||||||
use style::properties::style_structs;
|
use style::properties::style_structs;
|
||||||
use style::servo::restyle_damage::ServoRestyleDamage;
|
use style::servo::restyle_damage::ServoRestyleDamage;
|
||||||
|
@ -475,13 +476,18 @@ pub trait FragmentDisplayListBuilding {
|
||||||
display_list_section: DisplayListSection,
|
display_list_section: DisplayListSection,
|
||||||
absolute_bounds: &Rect<Au>);
|
absolute_bounds: &Rect<Au>);
|
||||||
|
|
||||||
/// Computes the background size for an image with the given background area according to the
|
/// Determines where to place an element background image or gradient.
|
||||||
/// rules in CSS-BACKGROUNDS § 3.9.
|
///
|
||||||
fn compute_background_image_size(&self,
|
/// Photos have their resolution as intrinsic size while gradients have
|
||||||
style: &ComputedValues,
|
/// no intrinsic size.
|
||||||
bounds: &Rect<Au>,
|
fn compute_background_placement(
|
||||||
image: &WebRenderImageInfo, index: usize)
|
&self,
|
||||||
-> Size2D<Au>;
|
state: &mut DisplayListBuildState,
|
||||||
|
style: &ComputedValues,
|
||||||
|
absolute_bounds: Rect<Au>,
|
||||||
|
intrinsic_size: Option<Size2D<Au>>,
|
||||||
|
index: usize
|
||||||
|
) -> BackgroundPlacement;
|
||||||
|
|
||||||
/// Adds the display items necessary to paint a webrender image of this fragment to the
|
/// Adds the display items necessary to paint a webrender image of this fragment to the
|
||||||
/// appropriate section of the display list.
|
/// appropriate section of the display list.
|
||||||
|
@ -489,8 +495,7 @@ pub trait FragmentDisplayListBuilding {
|
||||||
state: &mut DisplayListBuildState,
|
state: &mut DisplayListBuildState,
|
||||||
style: &ComputedValues,
|
style: &ComputedValues,
|
||||||
display_list_section: DisplayListSection,
|
display_list_section: DisplayListSection,
|
||||||
absolute_bounds: &Rect<Au>,
|
absolute_bounds: Rect<Au>,
|
||||||
clip: &LocalClip,
|
|
||||||
webrender_image: WebRenderImageInfo,
|
webrender_image: WebRenderImageInfo,
|
||||||
index: usize);
|
index: usize);
|
||||||
|
|
||||||
|
@ -510,7 +515,6 @@ pub trait FragmentDisplayListBuilding {
|
||||||
state: &mut DisplayListBuildState,
|
state: &mut DisplayListBuildState,
|
||||||
display_list_section: DisplayListSection,
|
display_list_section: DisplayListSection,
|
||||||
absolute_bounds: Rect<Au>,
|
absolute_bounds: Rect<Au>,
|
||||||
clip: &LocalClip,
|
|
||||||
gradient: &Gradient,
|
gradient: &Gradient,
|
||||||
style: &ComputedValues,
|
style: &ComputedValues,
|
||||||
index: usize);
|
index: usize);
|
||||||
|
@ -1024,6 +1028,176 @@ fn convert_ellipse_size_keyword(keyword: ShapeExtent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subtract offsets from a bounding box.
|
||||||
|
///
|
||||||
|
/// As an example if the bounds are the border-box and the border
|
||||||
|
/// is provided as offsets the result will be the padding-box.
|
||||||
|
fn calculate_inner_bounds(mut bounds: Rect<Au>, offsets: SideOffsets2D<Au>) -> Rect<Au> {
|
||||||
|
bounds.origin.x += offsets.left;
|
||||||
|
bounds.origin.y += offsets.top;
|
||||||
|
bounds.size.width -= offsets.horizontal();
|
||||||
|
bounds.size.height -= offsets.vertical();
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// For a given area and an image compute how big the
|
||||||
|
/// image should be displayed on the background.
|
||||||
|
fn compute_background_image_size(bg_size: BackgroundSize<LengthOrPercentageOrAuto>,
|
||||||
|
bounds_size: Size2D<Au>,
|
||||||
|
intrinsic_size: Option<Size2D<Au>>)
|
||||||
|
-> Size2D<Au> {
|
||||||
|
let own_size = if let Some(size) = intrinsic_size {
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
return match bg_size {
|
||||||
|
BackgroundSize::Cover | BackgroundSize::Contain => bounds_size,
|
||||||
|
BackgroundSize::Explicit { width, height } => {
|
||||||
|
Size2D::new(
|
||||||
|
MaybeAuto::from_style(width, bounds_size.width)
|
||||||
|
.specified_or_default(bounds_size.width),
|
||||||
|
MaybeAuto::from_style(height, bounds_size.height)
|
||||||
|
.specified_or_default(bounds_size.height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// If `image_aspect_ratio` < `bounds_aspect_ratio`, the image is tall; otherwise, it is
|
||||||
|
// wide.
|
||||||
|
let image_aspect_ratio = own_size.width.to_f32_px() / own_size.height.to_f32_px();
|
||||||
|
let bounds_aspect_ratio = bounds_size.width.to_f32_px() / bounds_size.height.to_f32_px();
|
||||||
|
match (bg_size, image_aspect_ratio < bounds_aspect_ratio) {
|
||||||
|
(BackgroundSize::Contain, false) | (BackgroundSize::Cover, true) => {
|
||||||
|
Size2D::new(bounds_size.width,
|
||||||
|
bounds_size.width.scale_by(image_aspect_ratio.recip()))
|
||||||
|
}
|
||||||
|
|
||||||
|
(BackgroundSize::Contain, true) | (BackgroundSize::Cover, false) => {
|
||||||
|
Size2D::new(bounds_size.height.scale_by(image_aspect_ratio),
|
||||||
|
bounds_size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
(BackgroundSize::Explicit { width, height: LengthOrPercentageOrAuto::Auto }, _) => {
|
||||||
|
let width = MaybeAuto::from_style(width, bounds_size.width)
|
||||||
|
.specified_or_default(own_size.width);
|
||||||
|
Size2D::new(width, width.scale_by(image_aspect_ratio.recip()))
|
||||||
|
}
|
||||||
|
|
||||||
|
(BackgroundSize::Explicit { width: LengthOrPercentageOrAuto::Auto, height }, _) => {
|
||||||
|
let height = MaybeAuto::from_style(height, bounds_size.height)
|
||||||
|
.specified_or_default(own_size.height);
|
||||||
|
Size2D::new(height.scale_by(image_aspect_ratio), height)
|
||||||
|
}
|
||||||
|
|
||||||
|
(BackgroundSize::Explicit { width, height }, _) => {
|
||||||
|
Size2D::new(MaybeAuto::from_style(width, bounds_size.width)
|
||||||
|
.specified_or_default(own_size.width),
|
||||||
|
MaybeAuto::from_style(height, bounds_size.height)
|
||||||
|
.specified_or_default(own_size.height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tile_image_round(position: &mut Au,
|
||||||
|
size: &mut Au,
|
||||||
|
absolute_anchor_origin: Au,
|
||||||
|
image_size: &mut Au) {
|
||||||
|
if *size == Au(0) || *image_size == Au(0) {
|
||||||
|
*position = Au(0);
|
||||||
|
*size =Au(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let number_of_tiles = (size.to_f32_px() / image_size.to_f32_px()).round().max(1.0);
|
||||||
|
*image_size = *size / (number_of_tiles as i32);
|
||||||
|
tile_image(position, size, absolute_anchor_origin, *image_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tile_image_spaced(position: &mut Au,
|
||||||
|
size: &mut Au,
|
||||||
|
tile_spacing: &mut Au,
|
||||||
|
absolute_anchor_origin: Au,
|
||||||
|
image_size: Au) {
|
||||||
|
if *size == Au(0) || image_size == Au(0) {
|
||||||
|
*position = Au(0);
|
||||||
|
*size = Au(0);
|
||||||
|
*tile_spacing = Au(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per the spec, if the space available is not enough for two images, just tile as
|
||||||
|
// normal but only display a single tile.
|
||||||
|
if image_size * 2 >= *size {
|
||||||
|
tile_image(position,
|
||||||
|
size,
|
||||||
|
absolute_anchor_origin,
|
||||||
|
image_size);
|
||||||
|
*tile_spacing = Au(0);
|
||||||
|
*size = image_size;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the box size, remove room for two tiles on the edges, and then calculate how many
|
||||||
|
// other tiles fit in between them.
|
||||||
|
let size_remaining = *size - (image_size * 2);
|
||||||
|
let num_middle_tiles = (size_remaining.to_f32_px() / image_size.to_f32_px()).floor() as i32;
|
||||||
|
|
||||||
|
// Allocate the remaining space as padding between tiles. background-position is ignored
|
||||||
|
// as per the spec, so the position is just the box origin. We are also ignoring
|
||||||
|
// background-attachment here, which seems unspecced when combined with
|
||||||
|
// background-repeat: space.
|
||||||
|
let space_for_middle_tiles = image_size * num_middle_tiles;
|
||||||
|
*tile_spacing = (size_remaining - space_for_middle_tiles) / (num_middle_tiles + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tile an image
|
||||||
|
fn tile_image(position: &mut Au,
|
||||||
|
size: &mut Au,
|
||||||
|
absolute_anchor_origin: Au,
|
||||||
|
image_size: Au) {
|
||||||
|
// Avoid division by zero below!
|
||||||
|
if image_size == Au(0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta_pixels = absolute_anchor_origin - *position;
|
||||||
|
let image_size_px = image_size.to_f32_px();
|
||||||
|
let tile_count = ((delta_pixels.to_f32_px() + image_size_px - 1.0) / image_size_px).floor();
|
||||||
|
let offset = image_size * (tile_count as i32);
|
||||||
|
let new_position = absolute_anchor_origin - offset;
|
||||||
|
*size = *position - new_position + *size;
|
||||||
|
*position = new_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For either the x or the y axis ajust various values to account for tiling.
|
||||||
|
///
|
||||||
|
/// This is done separately for both axes because the repeat keywords may differ.
|
||||||
|
fn tile_image_axis(repeat: BackgroundRepeatKeyword,
|
||||||
|
position: &mut Au,
|
||||||
|
size: &mut Au,
|
||||||
|
tile_size: &mut Au,
|
||||||
|
tile_spacing: &mut Au,
|
||||||
|
offset: Au,
|
||||||
|
clip_origin: Au,
|
||||||
|
clip_size: Au)
|
||||||
|
{
|
||||||
|
let absolute_anchor_origin = *position + offset;
|
||||||
|
match repeat {
|
||||||
|
BackgroundRepeatKeyword::NoRepeat => {
|
||||||
|
*position += offset;
|
||||||
|
*size = *tile_size;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
BackgroundRepeatKeyword::Repeat => (),
|
||||||
|
BackgroundRepeatKeyword::Space => tile_image_spaced(
|
||||||
|
position, size, tile_spacing, absolute_anchor_origin, *tile_size),
|
||||||
|
BackgroundRepeatKeyword::Round => tile_image_round(
|
||||||
|
position, size, absolute_anchor_origin, tile_size),
|
||||||
|
};
|
||||||
|
*position = clip_origin;
|
||||||
|
*size = clip_size;
|
||||||
|
tile_image(position, size, absolute_anchor_origin, *tile_size);
|
||||||
|
}
|
||||||
|
|
||||||
impl FragmentDisplayListBuilding for Fragment {
|
impl FragmentDisplayListBuilding for Fragment {
|
||||||
fn collect_stacking_contexts_for_blocklike_fragment(&mut self,
|
fn collect_stacking_contexts_for_blocklike_fragment(&mut self,
|
||||||
state: &mut StackingContextCollectionState)
|
state: &mut StackingContextCollectionState)
|
||||||
|
@ -1073,20 +1247,14 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
background.background_image.0.len() - 1);
|
background.background_image.0.len() - 1);
|
||||||
|
|
||||||
match *color_clip {
|
match *color_clip {
|
||||||
background_clip::single_value::T::BorderBox => {}
|
BackgroundClip::BorderBox => {}
|
||||||
background_clip::single_value::T::PaddingBox => {
|
BackgroundClip::PaddingBox => {
|
||||||
let border = style.logical_border_width().to_physical(style.writing_mode);
|
let border = style.logical_border_width().to_physical(style.writing_mode);
|
||||||
bounds.origin.x = bounds.origin.x + border.left;
|
bounds = calculate_inner_bounds(bounds, border);
|
||||||
bounds.origin.y = bounds.origin.y + border.top;
|
|
||||||
bounds.size.width = bounds.size.width - border.horizontal();
|
|
||||||
bounds.size.height = bounds.size.height - border.vertical();
|
|
||||||
}
|
}
|
||||||
background_clip::single_value::T::ContentBox => {
|
BackgroundClip::ContentBox => {
|
||||||
let border_padding = self.border_padding.to_physical(style.writing_mode);
|
let border_padding = self.border_padding.to_physical(style.writing_mode);
|
||||||
bounds.origin.x = bounds.origin.x + border_padding.left;
|
bounds = calculate_inner_bounds(bounds, border_padding);
|
||||||
bounds.origin.y = bounds.origin.y + border_padding.top;
|
|
||||||
bounds.size.width = bounds.size.width - border_padding.horizontal();
|
|
||||||
bounds.size.height = bounds.size.height - border_padding.vertical();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1125,7 +1293,6 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
self.build_display_list_for_background_gradient(state,
|
self.build_display_list_for_background_gradient(state,
|
||||||
display_list_section,
|
display_list_section,
|
||||||
*absolute_bounds,
|
*absolute_bounds,
|
||||||
&clip,
|
|
||||||
gradient,
|
gradient,
|
||||||
style,
|
style,
|
||||||
i);
|
i);
|
||||||
|
@ -1140,8 +1307,7 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
self.build_display_list_for_webrender_image(state,
|
self.build_display_list_for_webrender_image(state,
|
||||||
style,
|
style,
|
||||||
display_list_section,
|
display_list_section,
|
||||||
&bounds,
|
*absolute_bounds,
|
||||||
&clip,
|
|
||||||
webrender_image,
|
webrender_image,
|
||||||
i);
|
i);
|
||||||
}
|
}
|
||||||
|
@ -1168,8 +1334,7 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
self.build_display_list_for_webrender_image(state,
|
self.build_display_list_for_webrender_image(state,
|
||||||
style,
|
style,
|
||||||
display_list_section,
|
display_list_section,
|
||||||
&bounds,
|
*absolute_bounds,
|
||||||
&clip,
|
|
||||||
webrender_image,
|
webrender_image,
|
||||||
i);
|
i);
|
||||||
}
|
}
|
||||||
|
@ -1184,172 +1349,100 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_background_image_size(&self,
|
fn compute_background_placement(
|
||||||
style: &ComputedValues,
|
&self,
|
||||||
bounds: &Rect<Au>,
|
state: &mut DisplayListBuildState,
|
||||||
image: &WebRenderImageInfo,
|
style: &ComputedValues,
|
||||||
index: usize)
|
absolute_bounds: Rect<Au>,
|
||||||
-> Size2D<Au> {
|
intrinsic_size: Option<Size2D<Au>>,
|
||||||
// If `image_aspect_ratio` < `bounds_aspect_ratio`, the image is tall; otherwise, it is
|
index: usize
|
||||||
// wide.
|
) -> BackgroundPlacement {
|
||||||
let image_aspect_ratio = (image.width as f64) / (image.height as f64);
|
let bg = style.get_background();
|
||||||
let bounds_aspect_ratio = bounds.size.width.to_f64_px() / bounds.size.height.to_f64_px();
|
let bg_attachment = *get_cyclic(&bg.background_attachment.0, index);
|
||||||
let intrinsic_size = Size2D::new(Au::from_px(image.width as i32),
|
let bg_clip = *get_cyclic(&bg.background_clip.0, index);
|
||||||
Au::from_px(image.height as i32));
|
let bg_origin = *get_cyclic(&bg.background_origin.0, index);
|
||||||
let background_size = get_cyclic(&style.get_background().background_size.0, index).clone();
|
let bg_position_x = get_cyclic(&bg.background_position_x.0, index);
|
||||||
match (background_size, image_aspect_ratio < bounds_aspect_ratio) {
|
let bg_position_y = get_cyclic(&bg.background_position_y.0, index);
|
||||||
(BackgroundSize::Contain, false) | (BackgroundSize::Cover, true) => {
|
let bg_repeat = get_cyclic(&bg.background_repeat.0, index);
|
||||||
Size2D::new(bounds.size.width,
|
let bg_size = *get_cyclic(&bg.background_size.0, index);
|
||||||
Au::from_f64_px(bounds.size.width.to_f64_px() / image_aspect_ratio))
|
|
||||||
}
|
|
||||||
|
|
||||||
(BackgroundSize::Contain, true) | (BackgroundSize::Cover, false) => {
|
let css_clip = match bg_clip {
|
||||||
Size2D::new(Au::from_f64_px(bounds.size.height.to_f64_px() * image_aspect_ratio),
|
BackgroundClip::BorderBox => absolute_bounds,
|
||||||
bounds.size.height)
|
BackgroundClip::PaddingBox => calculate_inner_bounds(
|
||||||
}
|
absolute_bounds,
|
||||||
|
style.logical_border_width().to_physical(style.writing_mode),
|
||||||
|
),
|
||||||
|
BackgroundClip::ContentBox => calculate_inner_bounds(
|
||||||
|
absolute_bounds,
|
||||||
|
self.border_padding.to_physical(style.writing_mode),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
(BackgroundSize::Explicit { width, height: LengthOrPercentageOrAuto::Auto }, _) => {
|
let mut bounds = match bg_attachment {
|
||||||
let width = MaybeAuto::from_style(width, bounds.size.width)
|
BackgroundAttachment::Scroll => match bg_origin {
|
||||||
.specified_or_default(intrinsic_size.width);
|
BackgroundOrigin::BorderBox => absolute_bounds,
|
||||||
Size2D::new(width, Au::from_f64_px(width.to_f64_px() / image_aspect_ratio))
|
BackgroundOrigin::PaddingBox => calculate_inner_bounds(
|
||||||
}
|
absolute_bounds,
|
||||||
|
style.logical_border_width().to_physical(style.writing_mode),
|
||||||
|
),
|
||||||
|
BackgroundOrigin::ContentBox => calculate_inner_bounds(
|
||||||
|
absolute_bounds,
|
||||||
|
self.border_padding.to_physical(style.writing_mode),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
BackgroundAttachment::Fixed => Rect::new(
|
||||||
|
Point2D::origin(),
|
||||||
|
// Get current viewport
|
||||||
|
state.layout_context.shared_context().viewport_size(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
(BackgroundSize::Explicit { width: LengthOrPercentageOrAuto::Auto, height }, _) => {
|
let mut tile_size = compute_background_image_size(bg_size, bounds.size, intrinsic_size);
|
||||||
let height = MaybeAuto::from_style(height, bounds.size.height)
|
|
||||||
.specified_or_default(intrinsic_size.height);
|
|
||||||
Size2D::new(Au::from_f64_px(height.to_f64_px() * image_aspect_ratio), height)
|
|
||||||
}
|
|
||||||
|
|
||||||
(BackgroundSize::Explicit { width, height }, _) => {
|
let mut tile_spacing = Size2D::zero();
|
||||||
Size2D::new(MaybeAuto::from_style(width, bounds.size.width)
|
let own_position = bounds.size - intrinsic_size.unwrap_or(Size2D::zero());
|
||||||
.specified_or_default(intrinsic_size.width),
|
let pos_x = bg_position_x.to_used_value(own_position.width);
|
||||||
MaybeAuto::from_style(height, bounds.size.height)
|
let pos_y = bg_position_y.to_used_value(own_position.height);
|
||||||
.specified_or_default(intrinsic_size.height))
|
tile_image_axis(
|
||||||
}
|
bg_repeat.0,
|
||||||
}
|
&mut bounds.origin.x,
|
||||||
|
&mut bounds.size.width,
|
||||||
|
&mut tile_size.width,
|
||||||
|
&mut tile_spacing.width,
|
||||||
|
pos_x,
|
||||||
|
css_clip.origin.x,
|
||||||
|
css_clip.size.width);
|
||||||
|
tile_image_axis(
|
||||||
|
bg_repeat.1,
|
||||||
|
&mut bounds.origin.y,
|
||||||
|
&mut bounds.size.height,
|
||||||
|
&mut tile_size.height,
|
||||||
|
&mut tile_spacing.height,
|
||||||
|
pos_y,
|
||||||
|
css_clip.origin.y,
|
||||||
|
css_clip.size.height);
|
||||||
|
|
||||||
|
return BackgroundPlacement { bounds, tile_size, tile_spacing, css_clip }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_display_list_for_webrender_image(&self,
|
fn build_display_list_for_webrender_image(&self,
|
||||||
state: &mut DisplayListBuildState,
|
state: &mut DisplayListBuildState,
|
||||||
style: &ComputedValues,
|
style: &ComputedValues,
|
||||||
display_list_section: DisplayListSection,
|
display_list_section: DisplayListSection,
|
||||||
absolute_bounds: &Rect<Au>,
|
absolute_bounds: Rect<Au>,
|
||||||
clip: &LocalClip,
|
|
||||||
webrender_image: WebRenderImageInfo,
|
webrender_image: WebRenderImageInfo,
|
||||||
index: usize) {
|
index: usize) {
|
||||||
debug!("(building display list) building background image");
|
debug!("(building display list) building background image");
|
||||||
let background = style.get_background();
|
|
||||||
|
|
||||||
// Use `background-size` to get the size.
|
let image = Size2D::new(
|
||||||
let mut bounds = *absolute_bounds;
|
Au::from_px(webrender_image.width as i32),
|
||||||
let image_size = self.compute_background_image_size(style, &bounds,
|
Au::from_px(webrender_image.height as i32));
|
||||||
&webrender_image, index);
|
let placement = self.compute_background_placement(
|
||||||
|
state, style, absolute_bounds, Some(image), index);
|
||||||
// Background image should be positioned on the padding box basis.
|
|
||||||
let border = style.logical_border_width().to_physical(style.writing_mode);
|
|
||||||
|
|
||||||
// Use 'background-origin' to get the origin value.
|
|
||||||
let origin = get_cyclic(&background.background_origin.0, index);
|
|
||||||
let (mut origin_x, mut origin_y) = match *origin {
|
|
||||||
background_origin::single_value::T::PaddingBox => {
|
|
||||||
(Au(0), Au(0))
|
|
||||||
}
|
|
||||||
background_origin::single_value::T::BorderBox => {
|
|
||||||
(-border.left, -border.top)
|
|
||||||
}
|
|
||||||
background_origin::single_value::T::ContentBox => {
|
|
||||||
let border_padding = self.border_padding.to_physical(self.style.writing_mode);
|
|
||||||
(border_padding.left - border.left, border_padding.top - border.top)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use `background-attachment` to get the initial virtual origin
|
|
||||||
let attachment = get_cyclic(&background.background_attachment.0, index);
|
|
||||||
let (virtual_origin_x, virtual_origin_y) = match *attachment {
|
|
||||||
background_attachment::single_value::T::Scroll => {
|
|
||||||
(absolute_bounds.origin.x, absolute_bounds.origin.y)
|
|
||||||
}
|
|
||||||
background_attachment::single_value::T::Fixed => {
|
|
||||||
// If the ‘background-attachment’ value for this image is ‘fixed’, then
|
|
||||||
// 'background-origin' has no effect.
|
|
||||||
origin_x = Au(0);
|
|
||||||
origin_y = Au(0);
|
|
||||||
(Au(0), Au(0))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let horiz_position = *get_cyclic(&background.background_position_x.0, index);
|
|
||||||
let vert_position = *get_cyclic(&background.background_position_y.0, index);
|
|
||||||
// Use `background-position` to get the offset.
|
|
||||||
let horizontal_position = horiz_position.to_used_value(bounds.size.width - image_size.width);
|
|
||||||
let vertical_position = vert_position.to_used_value(bounds.size.height - image_size.height);
|
|
||||||
|
|
||||||
// The anchor position for this background, based on both the background-attachment
|
|
||||||
// and background-position properties.
|
|
||||||
let anchor_origin_x = border.left + virtual_origin_x + origin_x + horizontal_position;
|
|
||||||
let anchor_origin_y = border.top + virtual_origin_y + origin_y + vertical_position;
|
|
||||||
|
|
||||||
let mut tile_spacing = Size2D::zero();
|
|
||||||
let mut stretch_size = image_size;
|
|
||||||
|
|
||||||
// Adjust origin and size based on background-repeat
|
|
||||||
let background_repeat = get_cyclic(&background.background_repeat.0, index);
|
|
||||||
match background_repeat.0 {
|
|
||||||
BackgroundRepeatKeyword::NoRepeat => {
|
|
||||||
bounds.origin.x = anchor_origin_x;
|
|
||||||
bounds.size.width = image_size.width;
|
|
||||||
}
|
|
||||||
BackgroundRepeatKeyword::Repeat => {
|
|
||||||
ImageFragmentInfo::tile_image(&mut bounds.origin.x,
|
|
||||||
&mut bounds.size.width,
|
|
||||||
anchor_origin_x,
|
|
||||||
image_size.width);
|
|
||||||
}
|
|
||||||
BackgroundRepeatKeyword::Space => {
|
|
||||||
ImageFragmentInfo::tile_image_spaced(&mut bounds.origin.x,
|
|
||||||
&mut bounds.size.width,
|
|
||||||
&mut tile_spacing.width,
|
|
||||||
anchor_origin_x,
|
|
||||||
image_size.width);
|
|
||||||
|
|
||||||
}
|
|
||||||
BackgroundRepeatKeyword::Round => {
|
|
||||||
ImageFragmentInfo::tile_image_round(&mut bounds.origin.x,
|
|
||||||
&mut bounds.size.width,
|
|
||||||
anchor_origin_x,
|
|
||||||
&mut stretch_size.width);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match background_repeat.1 {
|
|
||||||
BackgroundRepeatKeyword::NoRepeat => {
|
|
||||||
bounds.origin.y = anchor_origin_y;
|
|
||||||
bounds.size.height = image_size.height;
|
|
||||||
}
|
|
||||||
BackgroundRepeatKeyword::Repeat => {
|
|
||||||
ImageFragmentInfo::tile_image(&mut bounds.origin.y,
|
|
||||||
&mut bounds.size.height,
|
|
||||||
anchor_origin_y,
|
|
||||||
image_size.height);
|
|
||||||
}
|
|
||||||
BackgroundRepeatKeyword::Space => {
|
|
||||||
ImageFragmentInfo::tile_image_spaced(&mut bounds.origin.y,
|
|
||||||
&mut bounds.size.height,
|
|
||||||
&mut tile_spacing.height,
|
|
||||||
anchor_origin_y,
|
|
||||||
image_size.height);
|
|
||||||
|
|
||||||
}
|
|
||||||
BackgroundRepeatKeyword::Round => {
|
|
||||||
ImageFragmentInfo::tile_image_round(&mut bounds.origin.y,
|
|
||||||
&mut bounds.size.height,
|
|
||||||
anchor_origin_y,
|
|
||||||
&mut stretch_size.height);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the image display item.
|
// Create the image display item.
|
||||||
let base = state.create_base_display_item(&bounds,
|
let base = state.create_base_display_item(&placement.bounds,
|
||||||
*clip,
|
LocalClip::Rect(placement.css_clip.to_rectf()),
|
||||||
self.node,
|
self.node,
|
||||||
style.get_cursor(Cursor::Default),
|
style.get_cursor(Cursor::Default),
|
||||||
display_list_section);
|
display_list_section);
|
||||||
|
@ -1359,8 +1452,8 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
base: base,
|
base: base,
|
||||||
webrender_image: webrender_image,
|
webrender_image: webrender_image,
|
||||||
image_data: None,
|
image_data: None,
|
||||||
stretch_size: stretch_size,
|
stretch_size: placement.tile_size,
|
||||||
tile_spacing: tile_spacing,
|
tile_spacing: placement.tile_spacing,
|
||||||
image_rendering: style.get_inheritedbox().image_rendering.clone(),
|
image_rendering: style.get_inheritedbox().image_rendering.clone(),
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
@ -1419,80 +1512,15 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
state: &mut DisplayListBuildState,
|
state: &mut DisplayListBuildState,
|
||||||
display_list_section: DisplayListSection,
|
display_list_section: DisplayListSection,
|
||||||
absolute_bounds: Rect<Au>,
|
absolute_bounds: Rect<Au>,
|
||||||
clip: &LocalClip,
|
|
||||||
gradient: &Gradient,
|
gradient: &Gradient,
|
||||||
style: &ComputedValues,
|
style: &ComputedValues,
|
||||||
index: usize) {
|
index: usize)
|
||||||
// Calculate where the first "tile" needs to be placed on one axis.
|
{
|
||||||
// * base is the beginning of the visible area
|
let placement = self.compute_background_placement(
|
||||||
// * start is the current "tile" position
|
state, style, absolute_bounds, None, index);
|
||||||
// * tile is the length of the "tile"
|
|
||||||
// Returns a difference between start and new start.
|
|
||||||
// It holds that base - tile < start - diff <= base
|
|
||||||
fn get_first_tile(base: Au, start: Au, tile: Au) -> Au {
|
|
||||||
if tile == Au(0) {
|
|
||||||
return Au(0);
|
|
||||||
}
|
|
||||||
if start > base {
|
|
||||||
((start - base) / tile + 1) * tile
|
|
||||||
} else {
|
|
||||||
((base - start) / tile) * tile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let bg = style.get_background();
|
let base = state.create_base_display_item(&placement.bounds,
|
||||||
let bg_origin = get_cyclic(&bg.background_origin.0, index).clone();
|
LocalClip::Rect(placement.css_clip.to_rectf()),
|
||||||
let bg_size = get_cyclic(&bg.background_size.0, index).clone();
|
|
||||||
let bg_position_x = get_cyclic(&bg.background_position_x.0, index).clone();
|
|
||||||
let bg_position_y = get_cyclic(&bg.background_position_y.0, index).clone();
|
|
||||||
|
|
||||||
let mut bounds = absolute_bounds;
|
|
||||||
|
|
||||||
match bg_origin {
|
|
||||||
BackgroundOrigin::BorderBox => {}
|
|
||||||
BackgroundOrigin::PaddingBox => {
|
|
||||||
let border = style.logical_border_width().to_physical(style.writing_mode);
|
|
||||||
bounds.origin.x += border.left;
|
|
||||||
bounds.origin.y += border.top;
|
|
||||||
bounds.size.width -= border.horizontal();
|
|
||||||
bounds.size.height -= border.vertical();
|
|
||||||
}
|
|
||||||
BackgroundOrigin::ContentBox => {
|
|
||||||
let border_padding = self.border_padding.to_physical(style.writing_mode);
|
|
||||||
bounds.origin.x += border_padding.left;
|
|
||||||
bounds.origin.y += border_padding.top;
|
|
||||||
bounds.size.width -= border_padding.horizontal();
|
|
||||||
bounds.size.height -= border_padding.vertical();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bounds.origin.x += bg_position_x.to_used_value(bounds.size.width);
|
|
||||||
bounds.origin.y += bg_position_y.to_used_value(bounds.size.height);
|
|
||||||
|
|
||||||
let tile = match bg_size {
|
|
||||||
BackgroundSize::Cover | BackgroundSize::Contain => bounds.size,
|
|
||||||
BackgroundSize::Explicit { width, height } => {
|
|
||||||
Size2D::new(
|
|
||||||
MaybeAuto::from_style(width, bounds.size.width)
|
|
||||||
.specified_or_default(bounds.size.width),
|
|
||||||
MaybeAuto::from_style(height, bounds.size.height)
|
|
||||||
.specified_or_default(bounds.size.height))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let diff_x = get_first_tile(absolute_bounds.origin.x,
|
|
||||||
bounds.origin.x,
|
|
||||||
tile.width);
|
|
||||||
let diff_y = get_first_tile(absolute_bounds.origin.y,
|
|
||||||
bounds.origin.y,
|
|
||||||
tile.height);
|
|
||||||
let tiled_bounds = Rect::new(
|
|
||||||
Point2D::new(bounds.origin.x - diff_x, bounds.origin.y - diff_y),
|
|
||||||
Size2D::new(absolute_bounds.size.width + diff_x,
|
|
||||||
absolute_bounds.size.height + diff_y));
|
|
||||||
|
|
||||||
let base = state.create_base_display_item(&tiled_bounds,
|
|
||||||
*clip,
|
|
||||||
self.node,
|
self.node,
|
||||||
style.get_cursor(Cursor::Default),
|
style.get_cursor(Cursor::Default),
|
||||||
display_list_section);
|
display_list_section);
|
||||||
|
@ -1500,19 +1528,20 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
let display_item = match gradient.kind {
|
let display_item = match gradient.kind {
|
||||||
GradientKind::Linear(angle_or_corner) => {
|
GradientKind::Linear(angle_or_corner) => {
|
||||||
let gradient = convert_linear_gradient(
|
let gradient = convert_linear_gradient(
|
||||||
tile,
|
placement.tile_size,
|
||||||
&gradient.items[..],
|
&gradient.items[..],
|
||||||
angle_or_corner,
|
angle_or_corner,
|
||||||
gradient.repeating);
|
gradient.repeating);
|
||||||
DisplayItem::Gradient(Box::new(GradientDisplayItem {
|
DisplayItem::Gradient(Box::new(GradientDisplayItem {
|
||||||
base: base,
|
base: base,
|
||||||
gradient: gradient,
|
gradient: gradient,
|
||||||
tile: tile,
|
tile: placement.tile_size,
|
||||||
|
tile_spacing: placement.tile_spacing,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
GradientKind::Radial(shape, center, _angle) => {
|
GradientKind::Radial(shape, center, _angle) => {
|
||||||
let gradient = convert_radial_gradient(
|
let gradient = convert_radial_gradient(
|
||||||
tile,
|
placement.tile_size,
|
||||||
&gradient.items[..],
|
&gradient.items[..],
|
||||||
shape,
|
shape,
|
||||||
center,
|
center,
|
||||||
|
@ -1520,7 +1549,8 @@ impl FragmentDisplayListBuilding for Fragment {
|
||||||
DisplayItem::RadialGradient(Box::new(RadialGradientDisplayItem {
|
DisplayItem::RadialGradient(Box::new(RadialGradientDisplayItem {
|
||||||
base: base,
|
base: base,
|
||||||
gradient: gradient,
|
gradient: gradient,
|
||||||
tile: tile,
|
tile: placement.tile_size,
|
||||||
|
tile_spacing: placement.tile_spacing,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -3327,3 +3357,19 @@ pub enum BorderPaintingMode<'a> {
|
||||||
/// Paint no borders.
|
/// Paint no borders.
|
||||||
Hidden,
|
Hidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct BackgroundPlacement {
|
||||||
|
/// Rendering bounds. The background will start in the uppper-left corner
|
||||||
|
/// and fill the whole area.
|
||||||
|
bounds: Rect<Au>,
|
||||||
|
/// Background tile size. Some backgrounds are repeated. These are the
|
||||||
|
/// dimensions of a single image of the background.
|
||||||
|
tile_size: Size2D<Au>,
|
||||||
|
/// Spacing between tiles. Some backgrounds are not repeated seamless
|
||||||
|
/// but have seams between them like tiles in real life.
|
||||||
|
tile_spacing: Size2D<Au>,
|
||||||
|
/// A clip area. While the background is rendered according to all the
|
||||||
|
/// measures above it is only shown within these bounds.
|
||||||
|
css_clip: Rect<Au>,
|
||||||
|
}
|
||||||
|
|
|
@ -421,77 +421,6 @@ impl ImageFragmentInfo {
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tile_image_round(position: &mut Au,
|
|
||||||
size: &mut Au,
|
|
||||||
absolute_anchor_origin: Au,
|
|
||||||
image_size: &mut Au) {
|
|
||||||
if *size == Au(0) || *image_size == Au(0) {
|
|
||||||
*position = Au(0);
|
|
||||||
*size =Au(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let number_of_tiles = (size.to_f32_px() / image_size.to_f32_px()).round().max(1.0);
|
|
||||||
*image_size = *size / (number_of_tiles as i32);
|
|
||||||
ImageFragmentInfo::tile_image(position, size, absolute_anchor_origin, *image_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tile_image_spaced(position: &mut Au,
|
|
||||||
size: &mut Au,
|
|
||||||
tile_spacing: &mut Au,
|
|
||||||
absolute_anchor_origin: Au,
|
|
||||||
image_size: Au) {
|
|
||||||
if *size == Au(0) || image_size == Au(0) {
|
|
||||||
*position = Au(0);
|
|
||||||
*size = Au(0);
|
|
||||||
*tile_spacing = Au(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per the spec, if the space available is not enough for two images, just tile as
|
|
||||||
// normal but only display a single tile.
|
|
||||||
if image_size * 2 >= *size {
|
|
||||||
ImageFragmentInfo::tile_image(position,
|
|
||||||
size,
|
|
||||||
absolute_anchor_origin,
|
|
||||||
image_size);
|
|
||||||
*tile_spacing = Au(0);
|
|
||||||
*size = image_size;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take the box size, remove room for two tiles on the edges, and then calculate how many
|
|
||||||
// other tiles fit in between them.
|
|
||||||
let size_remaining = *size - (image_size * 2);
|
|
||||||
let num_middle_tiles = (size_remaining.to_f32_px() / image_size.to_f32_px()).floor() as i32;
|
|
||||||
|
|
||||||
// Allocate the remaining space as padding between tiles. background-position is ignored
|
|
||||||
// as per the spec, so the position is just the box origin. We are also ignoring
|
|
||||||
// background-attachment here, which seems unspecced when combined with
|
|
||||||
// background-repeat: space.
|
|
||||||
let space_for_middle_tiles = image_size * num_middle_tiles;
|
|
||||||
*tile_spacing = (size_remaining - space_for_middle_tiles) / (num_middle_tiles + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tile an image
|
|
||||||
pub fn tile_image(position: &mut Au,
|
|
||||||
size: &mut Au,
|
|
||||||
absolute_anchor_origin: Au,
|
|
||||||
image_size: Au) {
|
|
||||||
// Avoid division by zero below!
|
|
||||||
if image_size == Au(0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let delta_pixels = absolute_anchor_origin - *position;
|
|
||||||
let image_size_px = image_size.to_f32_px();
|
|
||||||
let tile_count = ((delta_pixels.to_f32_px() + image_size_px - 1.0) / image_size_px).floor();
|
|
||||||
let offset = image_size * (tile_count as i32);
|
|
||||||
let new_position = absolute_anchor_origin - offset;
|
|
||||||
*size = *position - new_position + *size;
|
|
||||||
*position = new_position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fragment that represents an inline frame (iframe). This stores the frame ID so that the
|
/// A fragment that represents an inline frame (iframe). This stores the frame ID so that the
|
||||||
|
|
|
@ -442,7 +442,7 @@ impl WebRenderDisplayItemConverter for DisplayItem {
|
||||||
builder.push_gradient(&self.prim_info(),
|
builder.push_gradient(&self.prim_info(),
|
||||||
gradient,
|
gradient,
|
||||||
item.tile.to_sizef(),
|
item.tile.to_sizef(),
|
||||||
webrender_api::LayoutSize::zero());
|
item.tile_spacing.to_sizef());
|
||||||
}
|
}
|
||||||
DisplayItem::RadialGradient(ref item) => {
|
DisplayItem::RadialGradient(ref item) => {
|
||||||
let center = item.gradient.center.to_pointf();
|
let center = item.gradient.center.to_pointf();
|
||||||
|
@ -459,7 +459,7 @@ impl WebRenderDisplayItemConverter for DisplayItem {
|
||||||
builder.push_radial_gradient(&self.prim_info(),
|
builder.push_radial_gradient(&self.prim_info(),
|
||||||
gradient,
|
gradient,
|
||||||
item.tile.to_sizef(),
|
item.tile.to_sizef(),
|
||||||
webrender_api::LayoutSize::zero());
|
item.tile_spacing.to_sizef());
|
||||||
}
|
}
|
||||||
DisplayItem::Line(ref item) => {
|
DisplayItem::Line(ref item) => {
|
||||||
builder.push_line(&self.prim_info(),
|
builder.push_line(&self.prim_info(),
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
[background-334.html]
|
|
||||||
type: reftest
|
|
||||||
expected: FAIL
|
|
|
@ -1,3 +0,0 @@
|
||||||
[background-origin-006.html]
|
|
||||||
type: reftest
|
|
||||||
expected: FAIL
|
|
|
@ -1,3 +0,0 @@
|
||||||
[background-origin-007.html]
|
|
||||||
type: reftest
|
|
||||||
expected: FAIL
|
|
|
@ -1,3 +0,0 @@
|
||||||
[background-size-021.html]
|
|
||||||
type: reftest
|
|
||||||
expected: FAIL
|
|
|
@ -1,3 +0,0 @@
|
||||||
[css3-background-size-contain.html]
|
|
||||||
type: reftest
|
|
||||||
expected: FAIL
|
|
Loading…
Add table
Add a link
Reference in a new issue