/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use api::{BorderRadius, BorderSide, BorderStyle, ColorF, ColorU}; use api::{NormalBorder as ApiNormalBorder, RepeatMode}; use api::units::*; use crate::clip::ClipNodeId; use crate::ellipse::Ellipse; use euclid::vec2; use crate::scene_building::SceneBuilder; use crate::spatial_tree::SpatialNodeIndex; use crate::gpu_types::{BorderInstance, BorderSegment, BrushFlags}; use crate::prim_store::{BorderSegmentInfo, BrushSegment, NinePatchDescriptor}; use crate::prim_store::borders::{NormalBorderPrim, NormalBorderData}; use crate::util::{lerp, RectHelpers}; use crate::internal_types::LayoutPrimitiveInfo; use crate::segment::EdgeAaSegmentMask; // Using 2048 as the maximum radius in device space before which we // start stretching is up for debate. // the value must be chosen so that the corners will not use an // unreasonable amount of memory but should allow crisp corners in the // common cases. /// Maximum resolution in device pixels at which borders are rasterized. pub const MAX_BORDER_RESOLUTION: u32 = 2048; /// Maximum number of dots or dashes per segment to avoid freezing and filling up /// memory with unreasonable inputs. It would be better to address this by not building /// a list of per-dot information in the first place. pub const MAX_DASH_COUNT: u32 = 2048; // TODO(gw): Perhaps there is a better way to store // the border cache key than duplicating // all the border structs with hashable // variants... #[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct BorderRadiusAu { pub top_left: LayoutSizeAu, pub top_right: LayoutSizeAu, pub bottom_left: LayoutSizeAu, pub bottom_right: LayoutSizeAu, } impl From for BorderRadiusAu { fn from(radius: BorderRadius) -> BorderRadiusAu { BorderRadiusAu { top_left: radius.top_left.to_au(), top_right: radius.top_right.to_au(), bottom_right: radius.bottom_right.to_au(), bottom_left: radius.bottom_left.to_au(), } } } impl From for BorderRadius { fn from(radius: BorderRadiusAu) -> Self { BorderRadius { top_left: LayoutSize::from_au(radius.top_left), top_right: LayoutSize::from_au(radius.top_right), bottom_right: LayoutSize::from_au(radius.bottom_right), bottom_left: LayoutSize::from_au(radius.bottom_left), } } } #[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct BorderSideAu { pub color: ColorU, pub style: BorderStyle, } impl From for BorderSideAu { fn from(side: BorderSide) -> Self { BorderSideAu { color: side.color.into(), style: side.style, } } } impl From for BorderSide { fn from(side: BorderSideAu) -> Self { BorderSide { color: side.color.into(), style: side.style, } } } #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(Debug, Clone, Hash, Eq, MallocSizeOf, PartialEq)] pub struct NormalBorderAu { pub left: BorderSideAu, pub right: BorderSideAu, pub top: BorderSideAu, pub bottom: BorderSideAu, pub radius: BorderRadiusAu, /// Whether to apply anti-aliasing on the border corners. /// /// Note that for this to be `false` and work, this requires the borders to /// be solid, and no border-radius. pub do_aa: bool, } impl NormalBorderAu { // Construct a border based upon self with color pub fn with_color(&self, color: ColorU) -> Self { let mut b = self.clone(); b.left.color = color; b.right.color = color; b.top.color = color; b.bottom.color = color; b } } impl From for NormalBorderAu { fn from(border: ApiNormalBorder) -> Self { NormalBorderAu { left: border.left.into(), right: border.right.into(), top: border.top.into(), bottom: border.bottom.into(), radius: border.radius.into(), do_aa: border.do_aa, } } } impl From for ApiNormalBorder { fn from(border: NormalBorderAu) -> Self { ApiNormalBorder { left: border.left.into(), right: border.right.into(), top: border.top.into(), bottom: border.bottom.into(), radius: border.radius.into(), do_aa: border.do_aa, } } } /// Cache key that uniquely identifies a border /// segment in the render task cache. #[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct BorderSegmentCacheKey { pub size: LayoutSizeAu, pub radius: LayoutSizeAu, pub side0: BorderSideAu, pub side1: BorderSideAu, pub segment: BorderSegment, pub do_aa: bool, pub h_adjacent_corner_outer: LayoutPointAu, pub h_adjacent_corner_radius: LayoutSizeAu, pub v_adjacent_corner_outer: LayoutPointAu, pub v_adjacent_corner_radius: LayoutSizeAu, } pub fn ensure_no_corner_overlap( radius: &mut BorderRadius, size: LayoutSize, ) { let mut ratio = 1.0; let top_left_radius = &mut radius.top_left; let top_right_radius = &mut radius.top_right; let bottom_right_radius = &mut radius.bottom_right; let bottom_left_radius = &mut radius.bottom_left; let sum = top_left_radius.width + top_right_radius.width; if size.width < sum { ratio = f32::min(ratio, size.width / sum); } let sum = bottom_left_radius.width + bottom_right_radius.width; if size.width < sum { ratio = f32::min(ratio, size.width / sum); } let sum = top_left_radius.height + bottom_left_radius.height; if size.height < sum { ratio = f32::min(ratio, size.height / sum); } let sum = top_right_radius.height + bottom_right_radius.height; if size.height < sum { ratio = f32::min(ratio, size.height / sum); } if ratio < 1. { top_left_radius.width *= ratio; top_left_radius.height *= ratio; top_right_radius.width *= ratio; top_right_radius.height *= ratio; bottom_left_radius.width *= ratio; bottom_left_radius.height *= ratio; bottom_right_radius.width *= ratio; bottom_right_radius.height *= ratio; } } impl<'a> SceneBuilder<'a> { pub fn add_normal_border( &mut self, info: &LayoutPrimitiveInfo, border: &ApiNormalBorder, widths: LayoutSideOffsets, spatial_node_index: SpatialNodeIndex, clip_node_id: ClipNodeId, ) { let mut border = *border; ensure_no_corner_overlap(&mut border.radius, info.rect.size()); self.add_primitive( spatial_node_index, clip_node_id, info, Vec::new(), NormalBorderPrim { border: border.into(), widths: widths.to_au(), }, ); } } pub trait BorderSideHelpers { fn border_color(&self, is_inner_border: bool) -> ColorF; } impl BorderSideHelpers for BorderSide { fn border_color(&self, is_inner_border: bool) -> ColorF { let lighter = match self.style { BorderStyle::Inset => is_inner_border, BorderStyle::Outset => !is_inner_border, _ => return self.color, }; // The modulate colors below are not part of the specification. They are // derived from the Gecko source code and experimentation, and used to // modulate the colors in order to generate colors for the inset/outset // and groove/ridge border styles. // // NOTE(emilio): Gecko at least takes the background color into // account, should we do the same? Looks a bit annoying for this. // // NOTE(emilio): If you change this algorithm, do the same change on // get_colors_for_side in cs_border_segment.glsl. if self.color.r != 0.0 || self.color.g != 0.0 || self.color.b != 0.0 { let scale = if lighter { 1.0 } else { 2.0 / 3.0 }; return self.color.scale_rgb(scale) } let black = if lighter { 0.7 } else { 0.3 }; ColorF::new(black, black, black, self.color.a) } } /// The kind of border corner clip. #[repr(C)] #[derive(Copy, Debug, Clone, PartialEq)] pub enum BorderClipKind { DashCorner = 1, DashEdge = 2, Dot = 3, } fn compute_outer_and_clip_sign( corner_segment: BorderSegment, radius: DeviceSize, ) -> (DevicePoint, DeviceVector2D) { let outer_scale = match corner_segment { BorderSegment::TopLeft => DeviceVector2D::new(0.0, 0.0), BorderSegment::TopRight => DeviceVector2D::new(1.0, 0.0), BorderSegment::BottomRight => DeviceVector2D::new(1.0, 1.0), BorderSegment::BottomLeft => DeviceVector2D::new(0.0, 1.0), _ => panic!("bug: expected a corner segment"), }; let outer = DevicePoint::new( outer_scale.x * radius.width, outer_scale.y * radius.height, ); let clip_sign = DeviceVector2D::new( 1.0 - 2.0 * outer_scale.x, 1.0 - 2.0 * outer_scale.y, ); (outer, clip_sign) } fn write_dashed_corner_instances( corner_radius: DeviceSize, widths: DeviceSize, segment: BorderSegment, base_instance: &BorderInstance, instances: &mut Vec, ) -> Result<(), ()> { let ellipse = Ellipse::new(corner_radius); let average_border_width = 0.5 * (widths.width + widths.height); let (_half_dash, num_half_dashes) = compute_half_dash(average_border_width, ellipse.total_arc_length); if num_half_dashes == 0 { return Err(()); } let num_half_dashes = num_half_dashes.min(MAX_DASH_COUNT); let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius); let instance_count = num_half_dashes / 4 + 1; instances.reserve(instance_count as usize); let half_dash_arc_length = ellipse.total_arc_length / num_half_dashes as f32; let dash_length = 2. * half_dash_arc_length; let mut current_length = 0.; for i in 0..instance_count { let arc_length0 = current_length; current_length += if i == 0 { half_dash_arc_length } else { dash_length }; let arc_length1 = current_length; current_length += dash_length; let alpha = ellipse.find_angle_for_arc_length(arc_length0); let beta = ellipse.find_angle_for_arc_length(arc_length1); let (point0, tangent0) = ellipse.get_point_and_tangent(alpha); let (point1, tangent1) = ellipse.get_point_and_tangent(beta); let point0 = DevicePoint::new( outer.x + clip_sign.x * (corner_radius.width - point0.x), outer.y + clip_sign.y * (corner_radius.height - point0.y), ); let tangent0 = DeviceVector2D::new( -tangent0.x * clip_sign.x, -tangent0.y * clip_sign.y, ); let point1 = DevicePoint::new( outer.x + clip_sign.x * (corner_radius.width - point1.x), outer.y + clip_sign.y * (corner_radius.height - point1.y), ); let tangent1 = DeviceVector2D::new( -tangent1.x * clip_sign.x, -tangent1.y * clip_sign.y, ); instances.push(BorderInstance { flags: base_instance.flags | ((BorderClipKind::DashCorner as i32) << 24), clip_params: [ point0.x, point0.y, tangent0.x, tangent0.y, point1.x, point1.y, tangent1.x, tangent1.y, ], .. *base_instance }); } Ok(()) } fn write_dotted_corner_instances( corner_radius: DeviceSize, widths: DeviceSize, segment: BorderSegment, base_instance: &BorderInstance, instances: &mut Vec, ) -> Result<(), ()> { let mut corner_radius = corner_radius; if corner_radius.width < (widths.width / 2.0) { corner_radius.width = 0.0; } if corner_radius.height < (widths.height / 2.0) { corner_radius.height = 0.0; } let (ellipse, max_dot_count) = if corner_radius.width == 0. && corner_radius.height == 0. { (Ellipse::new(corner_radius), 1) } else { // The centers of dots follow an ellipse along the middle of the // border radius. let inner_radius = (corner_radius - widths * 0.5).abs(); let ellipse = Ellipse::new(inner_radius); // Allocate a "worst case" number of dot clips. This can be // calculated by taking the minimum edge radius, since that // will result in the maximum number of dots along the path. let min_diameter = widths.width.min(widths.height); // Get the number of circles (assuming spacing of one diameter // between dots). let max_dot_count = 0.5 * ellipse.total_arc_length / min_diameter; // Add space for one extra dot since they are centered at the // start of the arc. (ellipse, max_dot_count.ceil() as usize) }; if max_dot_count == 0 { return Err(()); } if max_dot_count == 1 { let dot_diameter = lerp(widths.width, widths.height, 0.5); instances.push(BorderInstance { flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24), clip_params: [ widths.width / 2.0, widths.height / 2.0, 0.5 * dot_diameter, 0., 0., 0., 0., 0., ], .. *base_instance }); return Ok(()); } let max_dot_count = max_dot_count.min(MAX_DASH_COUNT as usize); // FIXME(emilio): Should probably use SmallVec. let mut forward_dots = Vec::with_capacity(max_dot_count / 2 + 1); let mut back_dots = Vec::with_capacity(max_dot_count / 2 + 1); let mut leftover_arc_length = 0.0; // Alternate between adding dots at the start and end of the // ellipse arc. This ensures that we always end up with an exact // half dot at each end of the arc, to match up with the edges. forward_dots.push(DotInfo::new(widths.width, widths.width)); back_dots.push(DotInfo::new( ellipse.total_arc_length - widths.height, widths.height, )); let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius); for dot_index in 0 .. max_dot_count { let prev_forward_pos = *forward_dots.last().unwrap(); let prev_back_pos = *back_dots.last().unwrap(); // Select which end of the arc to place a dot from. // This just alternates between the start and end of // the arc, which ensures that there is always an // exact half-dot at each end of the ellipse. let going_forward = dot_index & 1 == 0; let (next_dot_pos, leftover) = if going_forward { let next_dot_pos = prev_forward_pos.arc_pos + 2.0 * prev_forward_pos.diameter; (next_dot_pos, prev_back_pos.arc_pos - next_dot_pos) } else { let next_dot_pos = prev_back_pos.arc_pos - 2.0 * prev_back_pos.diameter; (next_dot_pos, next_dot_pos - prev_forward_pos.arc_pos) }; // Use a lerp between each edge's dot // diameter, based on the linear distance // along the arc to get the diameter of the // dot at this arc position. let t = next_dot_pos / ellipse.total_arc_length; let dot_diameter = lerp(widths.width, widths.height, t); // If we can't fit a dot, bail out. if leftover < dot_diameter { leftover_arc_length = leftover; break; } // We can place a dot! let dot = DotInfo::new(next_dot_pos, dot_diameter); if going_forward { forward_dots.push(dot); } else { back_dots.push(dot); } } // Now step through the dots, and distribute any extra // leftover space on the arc between them evenly. Once // the final arc position is determined, generate the correct // arc positions and angles that get passed to the clip shader. let number_of_dots = forward_dots.len() + back_dots.len(); let extra_space_per_dot = leftover_arc_length / (number_of_dots - 1) as f32; let create_dot_data = |arc_length: f32, dot_radius: f32| -> [f32; 8] { // Represents the GPU data for drawing a single dot to a clip mask. The order // these are specified must stay in sync with the way this data is read in the // dot clip shader. let theta = ellipse.find_angle_for_arc_length(arc_length); let (center, _) = ellipse.get_point_and_tangent(theta); let center = DevicePoint::new( outer.x + clip_sign.x * (corner_radius.width - center.x), outer.y + clip_sign.y * (corner_radius.height - center.y), ); [center.x, center.y, dot_radius, 0.0, 0.0, 0.0, 0.0, 0.0] }; instances.reserve(number_of_dots); for (i, dot) in forward_dots.iter().enumerate() { let extra_dist = i as f32 * extra_space_per_dot; instances.push(BorderInstance { flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24), clip_params: create_dot_data(dot.arc_pos + extra_dist, 0.5 * dot.diameter), .. *base_instance }); } for (i, dot) in back_dots.iter().enumerate() { let extra_dist = i as f32 * extra_space_per_dot; instances.push(BorderInstance { flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24), clip_params: create_dot_data(dot.arc_pos - extra_dist, 0.5 * dot.diameter), .. *base_instance }); } Ok(()) } #[derive(Copy, Clone, Debug)] struct DotInfo { arc_pos: f32, diameter: f32, } impl DotInfo { fn new(arc_pos: f32, diameter: f32) -> DotInfo { DotInfo { arc_pos, diameter } } } /// Information needed to place and draw a border edge. #[derive(Debug)] struct EdgeInfo { /// Offset in local space to place the edge from origin. local_offset: f32, /// Size of the edge in local space. local_size: f32, /// Local stretch size for this edge (repeat past this). stretch_size: f32, } impl EdgeInfo { fn new( local_offset: f32, local_size: f32, stretch_size: f32, ) -> Self { Self { local_offset, local_size, stretch_size, } } } // Given a side width and the available space, compute the half-dash (half of // the 'on' segment) and the count of them for a given segment. fn compute_half_dash(side_width: f32, total_size: f32) -> (f32, u32) { let half_dash = side_width * 1.5; // 16k dashes should be enough for anyone let num_half_dashes = (total_size / half_dash).ceil().min(16.0 * 1024.0) as u32; if num_half_dashes == 0 { return (0., 0); } // TODO(emilio): Gecko has some other heuristics here to start with a full // dash when the border side is zero, for example. We might consider those // in the future. let num_half_dashes = if num_half_dashes % 4 != 0 { num_half_dashes + 4 - num_half_dashes % 4 } else { num_half_dashes }; let half_dash = total_size / num_half_dashes as f32; (half_dash, num_half_dashes) } // Get the needed size in device pixels for an edge, // based on the border style of that edge. This is used // to determine how big the render task should be. fn get_edge_info( style: BorderStyle, side_width: f32, avail_size: f32, ) -> EdgeInfo { // To avoid division by zero below. if side_width <= 0.0 || avail_size <= 0.0 { return EdgeInfo::new(0.0, 0.0, 0.0); } match style { BorderStyle::Dashed => { // Basically, two times the dash size. let (half_dash, _num_half_dashes) = compute_half_dash(side_width, avail_size); let stretch_size = 2.0 * 2.0 * half_dash; EdgeInfo::new(0., avail_size, stretch_size) } BorderStyle::Dotted => { let dot_and_space_size = 2.0 * side_width; if avail_size < dot_and_space_size * 0.75 { return EdgeInfo::new(0.0, 0.0, 0.0); } let approx_dot_count = avail_size / dot_and_space_size; let dot_count = approx_dot_count.floor().max(1.0); let used_size = dot_count * dot_and_space_size; let extra_space = avail_size - used_size; let stretch_size = dot_and_space_size; let offset = (extra_space * 0.5).round(); EdgeInfo::new(offset, used_size, stretch_size) } _ => { EdgeInfo::new(0.0, avail_size, 8.0) } } } /// Create the set of border segments and render task /// cache keys for a given CSS border. pub fn create_border_segments( size: LayoutSize, border: &ApiNormalBorder, widths: &LayoutSideOffsets, border_segments: &mut Vec, brush_segments: &mut Vec, ) { let rect = LayoutRect::from_size(size); let overlap = LayoutSize::new( (widths.left + widths.right - size.width).max(0.0), (widths.top + widths.bottom - size.height).max(0.0), ); let non_overlapping_widths = LayoutSideOffsets::new( widths.top - overlap.height / 2.0, widths.right - overlap.width / 2.0, widths.bottom - overlap.height / 2.0, widths.left - overlap.width / 2.0, ); let local_size_tl = LayoutSize::new( border.radius.top_left.width.max(widths.left), border.radius.top_left.height.max(widths.top), ); let local_size_tr = LayoutSize::new( border.radius.top_right.width.max(widths.right), border.radius.top_right.height.max(widths.top), ); let local_size_br = LayoutSize::new( border.radius.bottom_right.width.max(widths.right), border.radius.bottom_right.height.max(widths.bottom), ); let local_size_bl = LayoutSize::new( border.radius.bottom_left.width.max(widths.left), border.radius.bottom_left.height.max(widths.bottom), ); let top_edge_info = get_edge_info( border.top.style, widths.top, rect.width() - local_size_tl.width - local_size_tr.width, ); let bottom_edge_info = get_edge_info( border.bottom.style, widths.bottom, rect.width() - local_size_bl.width - local_size_br.width, ); let left_edge_info = get_edge_info( border.left.style, widths.left, rect.height() - local_size_tl.height - local_size_bl.height, ); let right_edge_info = get_edge_info( border.right.style, widths.right, rect.height() - local_size_tr.height - local_size_br.height, ); add_edge_segment( LayoutRect::from_floats( rect.min.x, rect.min.y + local_size_tl.height + left_edge_info.local_offset, rect.min.x + non_overlapping_widths.left, rect.min.y + local_size_tl.height + left_edge_info.local_offset + left_edge_info.local_size, ), &left_edge_info, border.left, non_overlapping_widths.left, BorderSegment::Left, EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT, brush_segments, border_segments, border.do_aa, ); add_edge_segment( LayoutRect::from_floats( rect.min.x + local_size_tl.width + top_edge_info.local_offset, rect.min.y, rect.min.x + local_size_tl.width + top_edge_info.local_offset + top_edge_info.local_size, rect.min.y + non_overlapping_widths.top, ), &top_edge_info, border.top, non_overlapping_widths.top, BorderSegment::Top, EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM, brush_segments, border_segments, border.do_aa, ); add_edge_segment( LayoutRect::from_floats( rect.min.x + rect.width() - non_overlapping_widths.right, rect.min.y + local_size_tr.height + right_edge_info.local_offset, rect.min.x + rect.width(), rect.min.y + local_size_tr.height + right_edge_info.local_offset + right_edge_info.local_size, ), &right_edge_info, border.right, non_overlapping_widths.right, BorderSegment::Right, EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT, brush_segments, border_segments, border.do_aa, ); add_edge_segment( LayoutRect::from_floats( rect.min.x + local_size_bl.width + bottom_edge_info.local_offset, rect.min.y + rect.height() - non_overlapping_widths.bottom, rect.min.x + local_size_bl.width + bottom_edge_info.local_offset + bottom_edge_info.local_size, rect.min.y + rect.height(), ), &bottom_edge_info, border.bottom, non_overlapping_widths.bottom, BorderSegment::Bottom, EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP, brush_segments, border_segments, border.do_aa, ); add_corner_segment( LayoutRect::from_floats( rect.min.x, rect.min.y, rect.min.x + local_size_tl.width, rect.min.y + local_size_tl.height, ), LayoutRect::from_floats( rect.min.x, rect.min.y, rect.max.x - non_overlapping_widths.right, rect.max.y - non_overlapping_widths.bottom ), border.left, border.top, LayoutSize::new(widths.left, widths.top), border.radius.top_left, BorderSegment::TopLeft, EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT, rect.top_right(), border.radius.top_right, rect.bottom_left(), border.radius.bottom_left, brush_segments, border_segments, border.do_aa, ); add_corner_segment( LayoutRect::from_floats( rect.min.x + rect.width() - local_size_tr.width, rect.min.y, rect.min.x + rect.width(), rect.min.y + local_size_tr.height, ), LayoutRect::from_floats( rect.min.x + non_overlapping_widths.left, rect.min.y, rect.max.x, rect.max.y - non_overlapping_widths.bottom, ), border.top, border.right, LayoutSize::new(widths.right, widths.top), border.radius.top_right, BorderSegment::TopRight, EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT, rect.min, border.radius.top_left, rect.max, border.radius.bottom_right, brush_segments, border_segments, border.do_aa, ); add_corner_segment( LayoutRect::from_floats( rect.min.x + rect.width() - local_size_br.width, rect.min.y + rect.height() - local_size_br.height, rect.min.x + rect.width(), rect.min.y + rect.height(), ), LayoutRect::from_floats( rect.min.x + non_overlapping_widths.left, rect.min.y + non_overlapping_widths.top, rect.max.x, rect.max.y, ), border.right, border.bottom, LayoutSize::new(widths.right, widths.bottom), border.radius.bottom_right, BorderSegment::BottomRight, EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT, rect.bottom_left(), border.radius.bottom_left, rect.top_right(), border.radius.top_right, brush_segments, border_segments, border.do_aa, ); add_corner_segment( LayoutRect::from_floats( rect.min.x, rect.min.y + rect.height() - local_size_bl.height, rect.min.x + local_size_bl.width, rect.min.y + rect.height(), ), LayoutRect::from_floats( rect.min.x, rect.min.y + non_overlapping_widths.top, rect.max.x - non_overlapping_widths.right, rect.max.y, ), border.bottom, border.left, LayoutSize::new(widths.left, widths.bottom), border.radius.bottom_left, BorderSegment::BottomLeft, EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT, rect.max, border.radius.bottom_right, rect.min, border.radius.top_left, brush_segments, border_segments, border.do_aa, ); } /// Computes the maximum scale that we allow for this set of border parameters. /// capping the scale will result in rendering very large corners at a lower /// resolution and stretching them, so they will have the right shape, but /// blurrier. pub fn get_max_scale_for_border( border_data: &NormalBorderData, ) -> LayoutToDeviceScale { let mut r = 1.0; for segment in &border_data.border_segments { let size = segment.local_task_size; r = size.width.max(size.height.max(r)); } LayoutToDeviceScale::new(MAX_BORDER_RESOLUTION as f32 / r) } fn add_segment( task_rect: DeviceRect, style0: BorderStyle, style1: BorderStyle, color0: ColorF, color1: ColorF, segment: BorderSegment, instances: &mut Vec, widths: DeviceSize, radius: DeviceSize, do_aa: bool, h_adjacent_corner_outer: DevicePoint, h_adjacent_corner_radius: DeviceSize, v_adjacent_corner_outer: DevicePoint, v_adjacent_corner_radius: DeviceSize, ) { let base_flags = (segment as i32) | ((style0 as i32) << 8) | ((style1 as i32) << 16) | ((do_aa as i32) << 28); let base_instance = BorderInstance { task_origin: DevicePoint::zero(), local_rect: task_rect, flags: base_flags, color0: color0.premultiplied(), color1: color1.premultiplied(), widths, radius, clip_params: [0.0; 8], }; match segment { BorderSegment::TopLeft | BorderSegment::TopRight | BorderSegment::BottomLeft | BorderSegment::BottomRight => { // TODO(gw): Similarly to the old border code, we don't correctly handle a a corner // that is dashed on one edge, and dotted on another. We can handle this // in the future by submitting two instances, each one with one side // color set to have an alpha of 0. if (style0 == BorderStyle::Dotted && style1 == BorderStyle::Dashed) || (style0 == BorderStyle::Dashed && style0 == BorderStyle::Dotted) { warn!("TODO: Handle a corner with dotted / dashed transition."); } let dashed_or_dotted_corner = match style0 { BorderStyle::Dashed => { write_dashed_corner_instances( radius, widths, segment, &base_instance, instances, ) } BorderStyle::Dotted => { write_dotted_corner_instances( radius, widths, segment, &base_instance, instances, ) } _ => Err(()), }; if dashed_or_dotted_corner.is_err() { let clip_params = [ h_adjacent_corner_outer.x, h_adjacent_corner_outer.y, h_adjacent_corner_radius.width, h_adjacent_corner_radius.height, v_adjacent_corner_outer.x, v_adjacent_corner_outer.y, v_adjacent_corner_radius.width, v_adjacent_corner_radius.height, ]; instances.push(BorderInstance { clip_params, ..base_instance }); } } BorderSegment::Top | BorderSegment::Bottom | BorderSegment::Right | BorderSegment::Left => { let is_vertical = segment == BorderSegment::Left || segment == BorderSegment::Right; match style0 { BorderStyle::Dashed => { let (x, y) = if is_vertical { let half_dash_size = task_rect.height() * 0.25; (0., half_dash_size) } else { let half_dash_size = task_rect.width() * 0.25; (half_dash_size, 0.) }; instances.push(BorderInstance { flags: base_flags | ((BorderClipKind::DashEdge as i32) << 24), clip_params: [ x, y, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ], ..base_instance }); } BorderStyle::Dotted => { let (x, y, r) = if is_vertical { (widths.width * 0.5, widths.width, widths.width * 0.5) } else { (widths.height, widths.height * 0.5, widths.height * 0.5) }; instances.push(BorderInstance { flags: base_flags | ((BorderClipKind::Dot as i32) << 24), clip_params: [ x, y, r, 0.0, 0.0, 0.0, 0.0, 0.0, ], ..base_instance }); } _ => { instances.push(base_instance); } } } } } /// Add a corner segment (if valid) to the list of /// border segments for this primitive. fn add_corner_segment( image_rect: LayoutRect, non_overlapping_rect: LayoutRect, side0: BorderSide, side1: BorderSide, widths: LayoutSize, radius: LayoutSize, segment: BorderSegment, edge_flags: EdgeAaSegmentMask, h_adjacent_corner_outer: LayoutPoint, h_adjacent_corner_radius: LayoutSize, v_adjacent_corner_outer: LayoutPoint, v_adjacent_corner_radius: LayoutSize, brush_segments: &mut Vec, border_segments: &mut Vec, do_aa: bool, ) { if side0.color.a <= 0.0 && side1.color.a <= 0.0 { return; } if widths.width <= 0.0 && widths.height <= 0.0 { return; } if side0.style.is_hidden() && side1.style.is_hidden() { return; } let segment_rect = match image_rect.intersection(&non_overlapping_rect) { Some(rect) => rect, None => { return; } }; let texture_rect = segment_rect .translate(-image_rect.min.to_vector()) .scale(1.0 / image_rect.width(), 1.0 / image_rect.height()); brush_segments.push( BrushSegment::new( segment_rect, /* may_need_clip_mask = */ true, edge_flags, [texture_rect.min.x, texture_rect.min.y, texture_rect.max.x, texture_rect.max.y], BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_TEXEL_RECT, ) ); // If the radii of the adjacent corners do not overlap with this segment, // then set the outer position to this segment's corner and the radii to zero. // That way the cache key is unaffected by non-overlapping corners, resulting // in fewer misses. let (h_corner_outer, h_corner_radius) = match segment { BorderSegment::TopLeft => { if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max.x { (h_adjacent_corner_outer, h_adjacent_corner_radius) } else { (LayoutPoint::new(image_rect.max.x, image_rect.min.y), LayoutSize::zero()) } } BorderSegment::TopRight => { if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min.x { (h_adjacent_corner_outer, h_adjacent_corner_radius) } else { (LayoutPoint::new(image_rect.min.x, image_rect.min.y), LayoutSize::zero()) } } BorderSegment::BottomRight => { if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min.x { (h_adjacent_corner_outer, h_adjacent_corner_radius) } else { (LayoutPoint::new(image_rect.min.x, image_rect.max.y), LayoutSize::zero()) } } BorderSegment::BottomLeft => { if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max.x { (h_adjacent_corner_outer, h_adjacent_corner_radius) } else { (image_rect.max, LayoutSize::zero()) } } _ => unreachable!() }; let (v_corner_outer, v_corner_radius) = match segment { BorderSegment::TopLeft => { if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max.y { (v_adjacent_corner_outer, v_adjacent_corner_radius) } else { (LayoutPoint::new(image_rect.min.x, image_rect.max.y), LayoutSize::zero()) } } BorderSegment::TopRight => { if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max.y { (v_adjacent_corner_outer, v_adjacent_corner_radius) } else { (image_rect.max, LayoutSize::zero()) } } BorderSegment::BottomRight => { if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min.y { (v_adjacent_corner_outer, v_adjacent_corner_radius) } else { (LayoutPoint::new(image_rect.max.x, image_rect.min.y), LayoutSize::zero()) } } BorderSegment::BottomLeft => { if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min.y { (v_adjacent_corner_outer, v_adjacent_corner_radius) } else { (LayoutPoint::new(image_rect.min.x, image_rect.min.y), LayoutSize::zero()) } } _ => unreachable!() }; border_segments.push(BorderSegmentInfo { local_task_size: image_rect.size(), cache_key: BorderSegmentCacheKey { do_aa, side0: side0.into(), side1: side1.into(), segment, radius: radius.to_au(), size: widths.to_au(), h_adjacent_corner_outer: (h_corner_outer - image_rect.min).to_point().to_au(), h_adjacent_corner_radius: h_corner_radius.to_au(), v_adjacent_corner_outer: (v_corner_outer - image_rect.min).to_point().to_au(), v_adjacent_corner_radius: v_corner_radius.to_au(), }, }); } /// Add an edge segment (if valid) to the list of /// border segments for this primitive. fn add_edge_segment( image_rect: LayoutRect, edge_info: &EdgeInfo, side: BorderSide, width: f32, segment: BorderSegment, edge_flags: EdgeAaSegmentMask, brush_segments: &mut Vec, border_segments: &mut Vec, do_aa: bool, ) { if side.color.a <= 0.0 { return; } if side.style.is_hidden() { return; } let (size, brush_flags) = match segment { BorderSegment::Left | BorderSegment::Right => { (LayoutSize::new(width, edge_info.stretch_size), BrushFlags::SEGMENT_REPEAT_Y) } BorderSegment::Top | BorderSegment::Bottom => { (LayoutSize::new(edge_info.stretch_size, width), BrushFlags::SEGMENT_REPEAT_X) } _ => { unreachable!(); } }; if image_rect.width() <= 0. || image_rect.height() <= 0. { return; } brush_segments.push( BrushSegment::new( image_rect, /* may_need_clip_mask = */ true, edge_flags, [0.0, 0.0, size.width, size.height], BrushFlags::SEGMENT_RELATIVE | brush_flags, ) ); border_segments.push(BorderSegmentInfo { local_task_size: size, cache_key: BorderSegmentCacheKey { do_aa, side0: side.into(), side1: side.into(), radius: LayoutSizeAu::zero(), size: size.to_au(), segment, h_adjacent_corner_outer: LayoutPointAu::zero(), h_adjacent_corner_radius: LayoutSizeAu::zero(), v_adjacent_corner_outer: LayoutPointAu::zero(), v_adjacent_corner_radius: LayoutSizeAu::zero(), }, }); } /// Build the set of border instances needed to draw a border /// segment into the render task cache. pub fn build_border_instances( cache_key: &BorderSegmentCacheKey, cache_size: DeviceIntSize, border: &ApiNormalBorder, scale: LayoutToDeviceScale, ) -> Vec { let mut instances = Vec::new(); let (side0, side1, flip0, flip1) = match cache_key.segment { BorderSegment::Left => (&border.left, &border.left, false, false), BorderSegment::Top => (&border.top, &border.top, false, false), BorderSegment::Right => (&border.right, &border.right, true, true), BorderSegment::Bottom => (&border.bottom, &border.bottom, true, true), BorderSegment::TopLeft => (&border.left, &border.top, false, false), BorderSegment::TopRight => (&border.top, &border.right, false, true), BorderSegment::BottomRight => (&border.right, &border.bottom, true, true), BorderSegment::BottomLeft => (&border.bottom, &border.left, true, false), }; let style0 = if side0.style.is_hidden() { side1.style } else { side0.style }; let style1 = if side1.style.is_hidden() { side0.style } else { side1.style }; let color0 = side0.border_color(flip0); let color1 = side1.border_color(flip1); let widths = (LayoutSize::from_au(cache_key.size) * scale).ceil(); let radius = (LayoutSize::from_au(cache_key.radius) * scale).ceil(); let h_corner_outer = (LayoutPoint::from_au(cache_key.h_adjacent_corner_outer) * scale).round(); let h_corner_radius = (LayoutSize::from_au(cache_key.h_adjacent_corner_radius) * scale).ceil(); let v_corner_outer = (LayoutPoint::from_au(cache_key.v_adjacent_corner_outer) * scale).round(); let v_corner_radius = (LayoutSize::from_au(cache_key.v_adjacent_corner_radius) * scale).ceil(); add_segment( DeviceRect::from_size(cache_size.to_f32()), style0, style1, color0, color1, cache_key.segment, &mut instances, widths, radius, border.do_aa, h_corner_outer, h_corner_radius, v_corner_outer, v_corner_radius, ); instances } impl NinePatchDescriptor { pub fn create_segments( &self, size: LayoutSize, ) -> Vec { let rect = LayoutRect::from_size(size); // Calculate the local texel coords of the slices. let px0 = 0.0; let px1 = self.slice.left as f32 / self.width as f32; let px2 = (self.width as f32 - self.slice.right as f32) / self.width as f32; let px3 = 1.0; let py0 = 0.0; let py1 = self.slice.top as f32 / self.height as f32; let py2 = (self.height as f32 - self.slice.bottom as f32) / self.height as f32; let py3 = 1.0; let tl_outer = LayoutPoint::new(rect.min.x, rect.min.y); let tl_inner = tl_outer + vec2(self.widths.left, self.widths.top); let tr_outer = LayoutPoint::new(rect.min.x + rect.width(), rect.min.y); let tr_inner = tr_outer + vec2(-self.widths.right, self.widths.top); let bl_outer = LayoutPoint::new(rect.min.x, rect.min.y + rect.height()); let bl_inner = bl_outer + vec2(self.widths.left, -self.widths.bottom); let br_outer = rect.max; let br_inner = br_outer - vec2(self.widths.right, self.widths.bottom); fn add_segment( segments: &mut Vec, rect: LayoutRect, uv_rect: TexelRect, repeat_horizontal: RepeatMode, repeat_vertical: RepeatMode, extra_flags: BrushFlags, ) { if uv_rect.uv1.x <= uv_rect.uv0.x || uv_rect.uv1.y <= uv_rect.uv0.y { return; } // Use segment relative interpolation for all // instances in this primitive. let mut brush_flags = BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_TEXEL_RECT | extra_flags; // Enable repeat modes on the segment. if repeat_horizontal == RepeatMode::Repeat { brush_flags |= BrushFlags::SEGMENT_REPEAT_X | BrushFlags::SEGMENT_REPEAT_X_CENTERED; } else if repeat_horizontal == RepeatMode::Round { brush_flags |= BrushFlags::SEGMENT_REPEAT_X | BrushFlags::SEGMENT_REPEAT_X_ROUND; } if repeat_vertical == RepeatMode::Repeat { brush_flags |= BrushFlags::SEGMENT_REPEAT_Y | BrushFlags::SEGMENT_REPEAT_Y_CENTERED; } else if repeat_vertical == RepeatMode::Round { brush_flags |= BrushFlags::SEGMENT_REPEAT_Y | BrushFlags::SEGMENT_REPEAT_Y_ROUND; } let segment = BrushSegment::new( rect, true, EdgeAaSegmentMask::empty(), [ uv_rect.uv0.x, uv_rect.uv0.y, uv_rect.uv1.x, uv_rect.uv1.y, ], brush_flags, ); segments.push(segment); } // Build the list of image segments let mut segments = Vec::new(); // Top left add_segment( &mut segments, LayoutRect::from_floats(tl_outer.x, tl_outer.y, tl_inner.x, tl_inner.y), TexelRect::new(px0, py0, px1, py1), RepeatMode::Stretch, RepeatMode::Stretch, BrushFlags::empty(), ); // Top right add_segment( &mut segments, LayoutRect::from_floats(tr_inner.x, tr_outer.y, tr_outer.x, tr_inner.y), TexelRect::new(px2, py0, px3, py1), RepeatMode::Stretch, RepeatMode::Stretch, BrushFlags::empty(), ); // Bottom right add_segment( &mut segments, LayoutRect::from_floats(br_inner.x, br_inner.y, br_outer.x, br_outer.y), TexelRect::new(px2, py2, px3, py3), RepeatMode::Stretch, RepeatMode::Stretch, BrushFlags::empty(), ); // Bottom left add_segment( &mut segments, LayoutRect::from_floats(bl_outer.x, bl_inner.y, bl_inner.x, bl_outer.y), TexelRect::new(px0, py2, px1, py3), RepeatMode::Stretch, RepeatMode::Stretch, BrushFlags::empty(), ); // Center if self.fill { add_segment( &mut segments, LayoutRect::from_floats(tl_inner.x, tl_inner.y, tr_inner.x, bl_inner.y), TexelRect::new(px1, py1, px2, py2), self.repeat_horizontal, self.repeat_vertical, BrushFlags::SEGMENT_NINEPATCH_MIDDLE, ); } // Add edge segments. // Top add_segment( &mut segments, LayoutRect::from_floats(tl_inner.x, tl_outer.y, tr_inner.x, tl_inner.y), TexelRect::new(px1, py0, px2, py1), self.repeat_horizontal, RepeatMode::Stretch, BrushFlags::empty(), ); // Bottom add_segment( &mut segments, LayoutRect::from_floats(bl_inner.x, bl_inner.y, br_inner.x, bl_outer.y), TexelRect::new(px1, py2, px2, py3), self.repeat_horizontal, RepeatMode::Stretch, BrushFlags::empty(), ); // Left add_segment( &mut segments, LayoutRect::from_floats(tl_outer.x, tl_inner.y, tl_inner.x, bl_inner.y), TexelRect::new(px0, py1, px1, py2), RepeatMode::Stretch, self.repeat_vertical, BrushFlags::empty(), ); // Right add_segment( &mut segments, LayoutRect::from_floats(tr_inner.x, tr_inner.y, br_outer.x, br_inner.y), TexelRect::new(px2, py1, px3, py2), RepeatMode::Stretch, self.repeat_vertical, BrushFlags::empty(), ); segments } }