mirror of
https://github.com/servo/servo.git
synced 2025-08-04 21:20:23 +01:00
gfx: Add elliptical border radius support
This commit is contained in:
parent
7474b29510
commit
3e5fb49b6f
9 changed files with 603 additions and 305 deletions
|
@ -961,12 +961,44 @@ pub struct BorderDisplayItem {
|
||||||
/// Information about the border radii.
|
/// Information about the border radii.
|
||||||
///
|
///
|
||||||
/// TODO(pcwalton): Elliptical radii.
|
/// TODO(pcwalton): Elliptical radii.
|
||||||
#[derive(Clone, Default, PartialEq, Debug, Copy, HeapSizeOf, Deserialize, Serialize)]
|
#[derive(Clone, PartialEq, Debug, Copy, HeapSizeOf, Deserialize, Serialize)]
|
||||||
pub struct BorderRadii<T> {
|
pub struct BorderRadii<T> {
|
||||||
pub top_left: T,
|
pub top_left: Size2D<T>,
|
||||||
pub top_right: T,
|
pub top_right: Size2D<T>,
|
||||||
pub bottom_right: T,
|
pub bottom_right: Size2D<T>,
|
||||||
pub bottom_left: T,
|
pub bottom_left: Size2D<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for BorderRadii<T> where T: Default, T: Clone {
|
||||||
|
fn default() -> Self {
|
||||||
|
let top_left = Size2D::new(Default::default(),
|
||||||
|
Default::default());
|
||||||
|
let top_right = Size2D::new(Default::default(),
|
||||||
|
Default::default());
|
||||||
|
let bottom_left = Size2D::new(Default::default(),
|
||||||
|
Default::default());
|
||||||
|
let bottom_right = Size2D::new(Default::default(),
|
||||||
|
Default::default());
|
||||||
|
BorderRadii { top_left: top_left,
|
||||||
|
top_right: top_right,
|
||||||
|
bottom_left: bottom_left,
|
||||||
|
bottom_right: bottom_right }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderRadii<Au> {
|
||||||
|
// Scale the border radii by the specified factor
|
||||||
|
pub fn scale_by(&self, s: f32) -> BorderRadii<Au> {
|
||||||
|
BorderRadii { top_left: BorderRadii::scale_corner_by(self.top_left, s),
|
||||||
|
top_right: BorderRadii::scale_corner_by(self.top_right, s),
|
||||||
|
bottom_left: BorderRadii::scale_corner_by(self.bottom_left, s),
|
||||||
|
bottom_right: BorderRadii::scale_corner_by(self.bottom_right, s) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the border corner radius by the specified factor
|
||||||
|
pub fn scale_corner_by(corner: Size2D<Au>, s: f32) -> Size2D<Au> {
|
||||||
|
Size2D { width: corner.width.scale_by(s), height: corner.height.scale_by(s) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> BorderRadii<T> where T: PartialEq + Zero {
|
impl<T> BorderRadii<T> where T: PartialEq + Zero {
|
||||||
|
@ -982,10 +1014,10 @@ impl<T> BorderRadii<T> where T: PartialEq + Zero + Clone {
|
||||||
/// Returns a set of border radii that all have the given value.
|
/// Returns a set of border radii that all have the given value.
|
||||||
pub fn all_same(value: T) -> BorderRadii<T> {
|
pub fn all_same(value: T) -> BorderRadii<T> {
|
||||||
BorderRadii {
|
BorderRadii {
|
||||||
top_left: value.clone(),
|
top_left: Size2D { width: value.clone(), height: value.clone() },
|
||||||
top_right: value.clone(),
|
top_right: Size2D { width: value.clone(), height: value.clone() },
|
||||||
bottom_right: value.clone(),
|
bottom_right: Size2D { width: value.clone(), height: value.clone() },
|
||||||
bottom_left: value.clone(),
|
bottom_left: Size2D { width: value.clone(), height: value.clone() },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,13 @@ struct Line {
|
||||||
end: Point2D<f32>,
|
end: Point2D<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CornerOrigin {
|
||||||
|
top_left: Point2D<f32>,
|
||||||
|
top_right: Point2D<f32>,
|
||||||
|
bottom_right: Point2D<f32>,
|
||||||
|
bottom_left: Point2D<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> PaintContext<'a> {
|
impl<'a> PaintContext<'a> {
|
||||||
pub fn draw_target(&self) -> &DrawTarget {
|
pub fn draw_target(&self) -> &DrawTarget {
|
||||||
&self.draw_target
|
&self.draw_target
|
||||||
|
@ -448,7 +455,7 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ellipse_rightmost_line_intersection_angle(e: Ellipse, l: Line) -> Option<f32> {
|
fn ellipse_rightmost_intersection(e: Ellipse, l: Line) -> Option<f32> {
|
||||||
match PaintContext::ellipse_line_intersection_angles(e, l) {
|
match PaintContext::ellipse_line_intersection_angles(e, l) {
|
||||||
(Some((p0, angle0)), Some((p1, _))) if p0.x > p1.x => Some(angle0),
|
(Some((p0, angle0)), Some((p1, _))) if p0.x > p1.x => Some(angle0),
|
||||||
(_, Some((_, angle1))) => Some(angle1),
|
(_, Some((_, angle1))) => Some(angle1),
|
||||||
|
@ -457,7 +464,7 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ellipse_leftmost_line_intersection_angle(e: Ellipse, l: Line) -> Option<f32> {
|
fn ellipse_leftmost_intersection(e: Ellipse, l: Line) -> Option<f32> {
|
||||||
match PaintContext::ellipse_line_intersection_angles(e, l) {
|
match PaintContext::ellipse_line_intersection_angles(e, l) {
|
||||||
(Some((p0, angle0)), Some((p1, _))) if p0.x < p1.x => Some(angle0),
|
(Some((p0, angle0)), Some((p1, _))) if p0.x < p1.x => Some(angle0),
|
||||||
(_, Some((_, angle1))) => Some(angle1),
|
(_, Some((_, angle1))) => Some(angle1),
|
||||||
|
@ -466,6 +473,155 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_zero_radius(radius: &Size2D<AzFloat>) -> bool {
|
||||||
|
radius.width <= 0. || radius.height <= 0.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from gecko:gfx/2d/PathHelpers.h:EllipseToBezier
|
||||||
|
fn ellipse_to_bezier(path_builder: &mut PathBuilder,
|
||||||
|
origin: Point2D<AzFloat>,
|
||||||
|
radius: Size2D<AzFloat>,
|
||||||
|
start_angle: f32,
|
||||||
|
end_angle: f32) {
|
||||||
|
if PaintContext::is_zero_radius(&radius) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate kappa constant for partial curve. The sign of angle in the
|
||||||
|
// tangent will actually ensure this is negative for a counter clockwise
|
||||||
|
// sweep, so changing signs later isn't needed.
|
||||||
|
let kappa_factor: f32 = (4.0f32 / 3.0f32) * ((end_angle - start_angle) / 4.).tan();
|
||||||
|
let kappa_x: f32 = kappa_factor * radius.width;
|
||||||
|
let kappa_y: f32 = kappa_factor * radius.height;
|
||||||
|
|
||||||
|
// We guarantee here the current point is the start point of the next
|
||||||
|
// curve segment.
|
||||||
|
let start_point = Point2D::new(origin.x + start_angle.cos() * radius.width,
|
||||||
|
origin.y + start_angle.sin() * radius.height);
|
||||||
|
|
||||||
|
path_builder.line_to(start_point);
|
||||||
|
let end_point = Point2D::new(origin.x + end_angle.cos() * radius.width,
|
||||||
|
origin.y + end_angle.sin() * radius.height);
|
||||||
|
|
||||||
|
let tangent_start = Point2D::new(-start_angle.sin(), start_angle.cos());
|
||||||
|
|
||||||
|
let cp1 = Point2D::new(start_point.x + tangent_start.x * kappa_x,
|
||||||
|
start_point.y + tangent_start.y * kappa_y);
|
||||||
|
|
||||||
|
let rev_tangent_end = Point2D::new(end_angle.sin(), -end_angle.cos());
|
||||||
|
|
||||||
|
let cp2 = Point2D::new(end_point.x + rev_tangent_end.x * kappa_x,
|
||||||
|
end_point.y + rev_tangent_end.y * kappa_y);
|
||||||
|
|
||||||
|
path_builder.bezier_curve_to(&cp1, &cp2, &end_point);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn inner_border_bounds(bounds: &Rect<f32>, border: &SideOffsets2D<f32>) -> Rect<f32> {
|
||||||
|
// T = top, B = bottom, L = left, R = right
|
||||||
|
let inner_TL = bounds.origin + Point2D::new(border.left, border.top);
|
||||||
|
let inner_BR = bounds.bottom_right() + Point2D::new(-border.right, -border.bottom);
|
||||||
|
Rect::new(inner_TL, Size2D::new(inner_BR.x - inner_TL.x, inner_BR.y - inner_TL.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn corner_bounds(bounds: &Rect<f32>,
|
||||||
|
border: &SideOffsets2D<f32>,
|
||||||
|
radii: &BorderRadii<AzFloat>) -> (CornerOrigin, SideOffsets2D<Size2D<f32>>) {
|
||||||
|
|
||||||
|
fn distance_to_elbow(radius: &Size2D<AzFloat>,
|
||||||
|
corner_width: f32,
|
||||||
|
corner_height: f32) -> Size2D<f32> {
|
||||||
|
if corner_width >= radius.width || corner_height >= radius.height {
|
||||||
|
Size2D::zero()
|
||||||
|
} else {
|
||||||
|
Size2D::new(radius.width - corner_width, radius.height - corner_height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// T = top, B = bottom, L = left, R = right
|
||||||
|
let origin_TL = bounds.origin + Point2D::new(radii.top_left.width, radii.top_left.height);
|
||||||
|
let origin_TR = bounds.top_right() + Point2D::new(-radii.top_right.width,
|
||||||
|
radii.top_right.height);
|
||||||
|
let origin_BR = bounds.bottom_right() + Point2D::new(-radii.bottom_right.width,
|
||||||
|
-radii.bottom_right.height);
|
||||||
|
let origin_BL = bounds.bottom_left() + Point2D::new(radii.bottom_left.width,
|
||||||
|
-radii.bottom_left.height);
|
||||||
|
|
||||||
|
let elbow_TL = distance_to_elbow(&radii.top_left, border.left, border.top);
|
||||||
|
let elbow_TR = distance_to_elbow(&radii.top_right, border.right, border.top);
|
||||||
|
let elbow_BR = distance_to_elbow(&radii.bottom_right, border.right, border.bottom);
|
||||||
|
let elbow_BL = distance_to_elbow(&radii.bottom_left, border.left, border.bottom);
|
||||||
|
(CornerOrigin { top_left: origin_TL,
|
||||||
|
top_right: origin_TR,
|
||||||
|
bottom_right: origin_BR,
|
||||||
|
bottom_left: origin_BL },
|
||||||
|
SideOffsets2D::new(elbow_TL, elbow_TR, elbow_BR, elbow_BL))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn draw_corner(path_builder: &mut PathBuilder,
|
||||||
|
origin: &Point2D<AzFloat>,
|
||||||
|
radius: &Size2D<AzFloat>,
|
||||||
|
inner_border: &Point2D<AzFloat>,
|
||||||
|
outer_border: &Point2D<AzFloat>,
|
||||||
|
dist_elbow: &Size2D<AzFloat>,
|
||||||
|
clockwise: bool) {
|
||||||
|
let rad_R: AzFloat = 0.;
|
||||||
|
let rad_BR = rad_R + f32::consts::FRAC_PI_4;
|
||||||
|
let rad_B = rad_BR + f32::consts::FRAC_PI_4;
|
||||||
|
let rad_BL = rad_B + f32::consts::FRAC_PI_4;
|
||||||
|
let rad_L = rad_BL + f32::consts::FRAC_PI_4;
|
||||||
|
let rad_TL = rad_L + f32::consts::FRAC_PI_4;
|
||||||
|
let rad_T = rad_TL + f32::consts::FRAC_PI_4;
|
||||||
|
|
||||||
|
fn compatible_borders_corner(border_corner_radius: &Size2D<f32>,
|
||||||
|
border1_width: f32,
|
||||||
|
border2_width: f32) -> bool {
|
||||||
|
(border_corner_radius.width - border_corner_radius.height).abs() <= f32::EPSILON &&
|
||||||
|
(border1_width - border2_width).abs() <= f32::EPSILON
|
||||||
|
}
|
||||||
|
|
||||||
|
if PaintContext::is_zero_radius(radius) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ellipse = Ellipse { origin: *origin, width: radius.width, height: radius.height };
|
||||||
|
let simple_border = compatible_borders_corner(&radius,
|
||||||
|
(outer_border.x - inner_border.x).abs(),
|
||||||
|
(outer_border.y - inner_border.y).abs());
|
||||||
|
let corner_angle = if simple_border {
|
||||||
|
f32::consts::FRAC_PI_4
|
||||||
|
} else {
|
||||||
|
if inner_border.x >= outer_border.x {
|
||||||
|
PaintContext::ellipse_leftmost_intersection(ellipse,
|
||||||
|
Line { start: *outer_border,
|
||||||
|
end: *inner_border }).unwrap()
|
||||||
|
} else {
|
||||||
|
PaintContext::ellipse_rightmost_intersection(ellipse,
|
||||||
|
Line { start: *inner_border,
|
||||||
|
end: *outer_border }).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (start_angle, end_angle) = match (inner_border.x <= outer_border.x,
|
||||||
|
inner_border.y >= outer_border.y) {
|
||||||
|
// TR corner - top border & right border
|
||||||
|
(true, true) => if clockwise { (-rad_B, rad_R - corner_angle) } else { (rad_R - corner_angle, rad_R) },
|
||||||
|
// BR corner - right border & bottom border
|
||||||
|
(true, false) => if clockwise { (rad_R, rad_R + corner_angle) } else { (rad_R + corner_angle, rad_B) },
|
||||||
|
// TL corner - left border & top border
|
||||||
|
(false, true) => if clockwise { (rad_L, rad_L + corner_angle) } else { (rad_L + corner_angle, rad_T) },
|
||||||
|
// BL corner - bottom border & left border
|
||||||
|
(false, false) => if clockwise { (rad_B, rad_L - corner_angle) } else { (rad_L - corner_angle, rad_L) },
|
||||||
|
};
|
||||||
|
if clockwise {
|
||||||
|
PaintContext::ellipse_to_bezier(path_builder, *origin, *radius, start_angle, end_angle);
|
||||||
|
PaintContext::ellipse_to_bezier(path_builder, *origin, *dist_elbow, end_angle, start_angle);
|
||||||
|
} else {
|
||||||
|
PaintContext::ellipse_to_bezier(path_builder, *origin, *dist_elbow, end_angle, start_angle);
|
||||||
|
PaintContext::ellipse_to_bezier(path_builder, *origin, *radius, start_angle, end_angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The following comment is wonderful, and stolen from
|
// The following comment is wonderful, and stolen from
|
||||||
// gecko:gfx/thebes/gfxContext.cpp:RoundedRectangle for reference.
|
// gecko:gfx/thebes/gfxContext.cpp:RoundedRectangle for reference.
|
||||||
//
|
//
|
||||||
|
@ -552,27 +708,17 @@ impl<'a> PaintContext<'a> {
|
||||||
bounds: &Rect<f32>,
|
bounds: &Rect<f32>,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
border: &SideOffsets2D<f32>,
|
border: &SideOffsets2D<f32>,
|
||||||
radius: &BorderRadii<AzFloat>,
|
radii: &BorderRadii<AzFloat>,
|
||||||
mode: BorderPathDrawingMode) {
|
mode: BorderPathDrawingMode) {
|
||||||
// T = top, B = bottom, L = left, R = right
|
// T = top, B = bottom, L = left, R = right
|
||||||
|
let inner = PaintContext::inner_border_bounds(bounds, &border);
|
||||||
let box_TL = bounds.origin;
|
let (box_TL, inner_TL,
|
||||||
let box_TR = box_TL + Point2D::new(bounds.size.width, 0.0);
|
box_TR, inner_TR,
|
||||||
let box_BL = box_TL + Point2D::new(0.0, bounds.size.height);
|
box_BR, inner_BR,
|
||||||
let box_BR = box_TL + Point2D::new(bounds.size.width, bounds.size.height);
|
box_BL, inner_BL) = (bounds.origin, inner.origin,
|
||||||
|
bounds.top_right(), inner.top_right(),
|
||||||
let inner_TL = box_TL + Point2D::new(border.left, border.top);
|
bounds.bottom_right(), inner.bottom_right(),
|
||||||
let inner_TR = box_TR + Point2D::new(-border.right, border.top);
|
bounds.bottom_left(), inner.bottom_left());
|
||||||
let inner_BR = box_BR + Point2D::new(-border.right, -border.bottom);
|
|
||||||
let inner_BL = box_BL + Point2D::new(border.left, -border.bottom);
|
|
||||||
|
|
||||||
let rad_R: AzFloat = 0.;
|
|
||||||
let rad_BR = rad_R + f32::consts::FRAC_PI_4;
|
|
||||||
let rad_B = rad_BR + f32::consts::FRAC_PI_4;
|
|
||||||
let rad_BL = rad_B + f32::consts::FRAC_PI_4;
|
|
||||||
let rad_L = rad_BL + f32::consts::FRAC_PI_4;
|
|
||||||
let rad_TL = rad_L + f32::consts::FRAC_PI_4;
|
|
||||||
let rad_T = rad_TL + f32::consts::FRAC_PI_4;
|
|
||||||
|
|
||||||
fn dx(x: AzFloat) -> Point2D<AzFloat> {
|
fn dx(x: AzFloat) -> Point2D<AzFloat> {
|
||||||
Point2D::new(x, 0.)
|
Point2D::new(x, 0.)
|
||||||
|
@ -590,44 +736,23 @@ impl<'a> PaintContext<'a> {
|
||||||
Point2D::new(0., if cond { dy } else { 0. })
|
Point2D::new(0., if cond { dy } else { 0. })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compatible_borders_corner(border1_width: f32, border2_width: f32) -> bool {
|
let (corner_origin, elbow) =
|
||||||
(border1_width - border2_width).abs() <= f32::EPSILON
|
PaintContext::corner_bounds(bounds, border, radii);
|
||||||
}
|
|
||||||
|
|
||||||
let distance_to_elbow_TL =
|
let (elbow_TL, elbow_TR, elbow_BR, elbow_BL) =
|
||||||
if border.top == border.left {
|
(elbow.top, elbow.right, elbow.bottom, elbow.left);
|
||||||
(radius.top_left - border.top).max(0.)
|
|
||||||
} else {
|
|
||||||
0.
|
|
||||||
};
|
|
||||||
let distance_to_elbow_TR =
|
|
||||||
if border.top == border.right {
|
|
||||||
(radius.top_right - border.top).max(0.)
|
|
||||||
} else {
|
|
||||||
0.
|
|
||||||
};
|
|
||||||
let distance_to_elbow_BR =
|
|
||||||
if border.right == border.bottom {
|
|
||||||
(radius.bottom_right - border.bottom).max(0.)
|
|
||||||
} else {
|
|
||||||
0.
|
|
||||||
};
|
|
||||||
let distance_to_elbow_BL =
|
|
||||||
if border.left == border.bottom {
|
|
||||||
(radius.bottom_left - border.bottom).max(0.)
|
|
||||||
} else {
|
|
||||||
0.
|
|
||||||
};
|
|
||||||
|
|
||||||
match direction {
|
match direction {
|
||||||
Direction::Top => {
|
Direction::Top => {
|
||||||
let edge_TL = box_TL + dx(radius.top_left.max(border.left));
|
let edge_TL = box_TL + dx(radii.top_left.width.max(border.left));
|
||||||
let edge_TR = box_TR + dx(-radius.top_right.max(border.right));
|
let edge_TR = box_TR + dx(-radii.top_right.width.max(border.right));
|
||||||
let edge_BR = box_TR + dx(-border.right - distance_to_elbow_TR) + dy(border.top);
|
let edge_BR = box_TR + dx(-border.right - elbow_TR.width) + dy(border.top);
|
||||||
let edge_BL = box_TL + dx(border.left + distance_to_elbow_TL) + dy(border.top);
|
let edge_BL = box_TL + dx(border.left + elbow_TL.width) + dy(border.top);
|
||||||
|
|
||||||
let corner_TL = edge_TL + dx_if(radius.top_left == 0., -border.left);
|
let corner_TL = edge_TL + dx_if(PaintContext::is_zero_radius(&radii.top_left),
|
||||||
let corner_TR = edge_TR + dx_if(radius.top_right == 0., border.right);
|
-border.left);
|
||||||
|
let corner_TR = edge_TR + dx_if(PaintContext::is_zero_radius(&radii.top_right),
|
||||||
|
border.right);
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
|
@ -636,25 +761,13 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_TR),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_TR),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
if radius.top_right != 0. {
|
&corner_origin.top_right,
|
||||||
// the origin is the center of the arcs we're about to draw.
|
&radii.top_right,
|
||||||
let origin = edge_TR + Point2D::new((border.right - radius.top_right).max(0.),
|
&inner_TR,
|
||||||
radius.top_right);
|
&box_TR,
|
||||||
let angle = if compatible_borders_corner(border.top, border.right) {
|
&elbow_TR,
|
||||||
f32::consts::FRAC_PI_4
|
true);
|
||||||
} else {
|
|
||||||
let line = Line { start: inner_TR, end: box_TR };
|
|
||||||
let ellipse = Ellipse { origin: origin, width: radius.top_right, height: radius.top_right };
|
|
||||||
PaintContext::ellipse_rightmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
path_builder.arc(origin, radius.top_right, rad_T, rad_R - angle, false);
|
|
||||||
if distance_to_elbow_TR != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_TR, rad_R - angle, rad_T, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
path_builder.line_to(edge_BR);
|
path_builder.line_to(edge_BR);
|
||||||
|
@ -662,32 +775,25 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_BL),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_BL),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
if radius.top_left != 0. {
|
&corner_origin.top_left,
|
||||||
let origin = edge_TL + Point2D::new(-(border.left - radius.top_left).max(0.),
|
&radii.top_left,
|
||||||
radius.top_left);
|
&inner_TL,
|
||||||
let angle = if compatible_borders_corner(border.top, border.left) {
|
&box_TL,
|
||||||
f32::consts::FRAC_PI_4
|
&elbow_TL,
|
||||||
} else {
|
false);
|
||||||
let line = Line { start: box_TL, end: inner_TL };
|
|
||||||
let ellipse = Ellipse { origin: origin, width: radius.top_left, height: radius.top_left };
|
|
||||||
PaintContext::ellipse_leftmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
if distance_to_elbow_TL != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_TL, rad_T, rad_L + angle, true);
|
|
||||||
}
|
|
||||||
path_builder.arc(origin, radius.top_left, rad_L + angle, rad_T, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Direction::Left => {
|
Direction::Left => {
|
||||||
let edge_TL = box_TL + dy(radius.top_left.max(border.top));
|
let edge_TL = box_TL + dy(radii.top_left.height.max(border.top));
|
||||||
let edge_BL = box_BL + dy(-radius.bottom_left.max(border.bottom));
|
let edge_BL = box_BL + dy(-radii.bottom_left.height.max(border.bottom));
|
||||||
let edge_TR = box_TL + dx(border.left) + dy(border.top + distance_to_elbow_TL);
|
let edge_TR = box_TL + dx(border.left) + dy(border.top + elbow_TL.height);
|
||||||
let edge_BR = box_BL + dx(border.left) + dy(-border.bottom - distance_to_elbow_BL);
|
let edge_BR = box_BL + dx(border.left) + dy(-border.bottom -
|
||||||
|
elbow_BL.height);
|
||||||
|
|
||||||
let corner_TL = edge_TL + dy_if(radius.top_left == 0., -border.top);
|
let corner_TL = edge_TL + dy_if(PaintContext::is_zero_radius(&radii.top_left),
|
||||||
let corner_BL = edge_BL + dy_if(radius.bottom_left == 0., border.bottom);
|
-border.top);
|
||||||
|
let corner_BL = edge_BL + dy_if(PaintContext::is_zero_radius(&radii.bottom_left),
|
||||||
|
border.bottom);
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
|
@ -696,25 +802,13 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_TL),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_TL),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
if radius.top_left != 0. {
|
&corner_origin.top_left,
|
||||||
let origin = edge_TL + Point2D::new(radius.top_left,
|
&radii.top_left,
|
||||||
-(border.top - radius.top_left).max(0.));
|
&inner_TL,
|
||||||
|
&box_TL,
|
||||||
let angle = if compatible_borders_corner(border.top, border.left) {
|
&elbow_TL,
|
||||||
f32::consts::FRAC_PI_4
|
true);
|
||||||
} else {
|
|
||||||
let line = Line { start: box_TL, end: inner_TL };
|
|
||||||
let ellipse = Ellipse { origin: origin, width: radius.top_left, height: radius.top_left };
|
|
||||||
PaintContext::ellipse_leftmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
path_builder.arc(origin, radius.top_left, rad_L, rad_L + angle, false);
|
|
||||||
if distance_to_elbow_TL != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_TL, rad_L + angle, rad_L, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
path_builder.line_to(edge_TR);
|
path_builder.line_to(edge_TR);
|
||||||
|
@ -722,35 +816,25 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_BR),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_BR),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
if radius.bottom_left != 0. {
|
&corner_origin.bottom_left,
|
||||||
let origin = edge_BL +
|
&radii.bottom_left,
|
||||||
Point2D::new(radius.bottom_left,
|
&inner_BL,
|
||||||
(border.bottom - radius.bottom_left).max(0.));
|
&box_BL,
|
||||||
let angle = if compatible_borders_corner(border.bottom, border.left) {
|
&elbow_BL,
|
||||||
f32::consts::FRAC_PI_4
|
false);
|
||||||
} else {
|
|
||||||
let line = Line { start: box_BL, end: inner_BL };
|
|
||||||
let ellipse = Ellipse { origin: origin,
|
|
||||||
width: radius.bottom_left,
|
|
||||||
height: radius.bottom_left };
|
|
||||||
PaintContext::ellipse_leftmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
if distance_to_elbow_BL != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_BL, rad_L, rad_L - angle, true);
|
|
||||||
}
|
|
||||||
path_builder.arc(origin, radius.bottom_left, rad_L - angle, rad_L, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Direction::Right => {
|
Direction::Right => {
|
||||||
let edge_TR = box_TR + dy(radius.top_right.max(border.top));
|
let edge_TR = box_TR + dy(radii.top_right.height.max(border.top));
|
||||||
let edge_BR = box_BR + dy(-radius.bottom_right.max(border.bottom));
|
let edge_BR = box_BR + dy(-radii.bottom_right.height.max(border.bottom));
|
||||||
let edge_TL = box_TR + dx(-border.right) + dy(border.top + distance_to_elbow_TR);
|
let edge_TL = box_TR + dx(-border.right) + dy(border.top + elbow_TR.height);
|
||||||
let edge_BL = box_BR + dx(-border.right) + dy(-border.bottom - distance_to_elbow_BR);
|
let edge_BL = box_BR + dx(-border.right) + dy(-border.bottom -
|
||||||
|
elbow_BR.height);
|
||||||
|
|
||||||
let corner_TR = edge_TR + dy_if(radius.top_right == 0., -border.top);
|
let corner_TR = edge_TR + dy_if(PaintContext::is_zero_radius(&radii.top_right),
|
||||||
let corner_BR = edge_BR + dy_if(radius.bottom_right == 0., border.bottom);
|
-border.top);
|
||||||
|
let corner_BR = edge_BR + dy_if(PaintContext::is_zero_radius(&radii.bottom_right),
|
||||||
|
border.bottom);
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
|
@ -759,24 +843,13 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_TL),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_TL),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
if radius.top_right != 0. {
|
&corner_origin.top_right,
|
||||||
let origin = edge_TR + Point2D::new(-radius.top_right,
|
&radii.top_right,
|
||||||
-(border.top - radius.top_right).max(0.));
|
&inner_TR,
|
||||||
let angle = if compatible_borders_corner(border.top, border.right) {
|
&box_TR,
|
||||||
f32::consts::FRAC_PI_4
|
&elbow_TR,
|
||||||
} else {
|
false);
|
||||||
let line = Line { start: inner_TR, end: box_TR };
|
|
||||||
let ellipse = Ellipse { origin: origin, width: radius.top_right, height: radius.top_right };
|
|
||||||
PaintContext::ellipse_rightmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
if distance_to_elbow_TR != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_TR, rad_R, rad_R - angle, true);
|
|
||||||
}
|
|
||||||
path_builder.arc(origin, radius.top_right, rad_R - angle, rad_R, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
path_builder.line_to(corner_TR);
|
path_builder.line_to(corner_TR);
|
||||||
|
@ -784,35 +857,27 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_BR),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_BR),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
|
&corner_origin.bottom_right,
|
||||||
|
&radii.bottom_right,
|
||||||
|
&inner_BR,
|
||||||
|
&box_BR,
|
||||||
|
&elbow_BR,
|
||||||
|
true);
|
||||||
|
|
||||||
if radius.bottom_right != 0. {
|
|
||||||
let origin = edge_BR +
|
|
||||||
Point2D::new(-radius.bottom_right,
|
|
||||||
(border.bottom - radius.bottom_right).max(0.));
|
|
||||||
let angle = if compatible_borders_corner(border.bottom, border.right) {
|
|
||||||
f32::consts::FRAC_PI_4
|
|
||||||
} else {
|
|
||||||
let line = Line { start: inner_BR, end: box_BR };
|
|
||||||
let ellipse = Ellipse { origin: origin,
|
|
||||||
width: radius.bottom_right,
|
|
||||||
height: radius.bottom_right };
|
|
||||||
PaintContext::ellipse_rightmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
path_builder.arc(origin, radius.bottom_right, rad_R, rad_R + angle, false);
|
|
||||||
if distance_to_elbow_BR != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_BR, rad_R + angle, rad_R, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Direction::Bottom => {
|
Direction::Bottom => {
|
||||||
let edge_BL = box_BL + dx(radius.bottom_left.max(border.left));
|
let edge_BL = box_BL + dx(radii.bottom_left.width.max(border.left));
|
||||||
let edge_BR = box_BR + dx(-radius.bottom_right.max(border.right));
|
let edge_BR = box_BR + dx(-radii.bottom_right.width.max(border.right));
|
||||||
let edge_TL = box_BL + dy(-border.bottom) + dx(border.left + distance_to_elbow_BL);
|
let edge_TL = box_BL + dy(-border.bottom) + dx(border.left +
|
||||||
let edge_TR = box_BR + dy(-border.bottom) + dx(-border.right - distance_to_elbow_BR);
|
elbow_BL.width);
|
||||||
|
let edge_TR = box_BR + dy(-border.bottom) + dx(-border.right -
|
||||||
|
elbow_BR.width);
|
||||||
|
|
||||||
let corner_BR = edge_BR + dx_if(radius.bottom_right == 0., border.right);
|
let corner_BR = edge_BR + dx_if(PaintContext::is_zero_radius(&radii.bottom_right),
|
||||||
let corner_BL = edge_BL + dx_if(radius.bottom_left == 0., -border.left);
|
border.right);
|
||||||
|
let corner_BL = edge_BL + dx_if(PaintContext::is_zero_radius(&radii.bottom_left),
|
||||||
|
-border.left);
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
|
@ -821,26 +886,13 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_TR),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(edge_TR),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
if radius.bottom_right != 0. {
|
&corner_origin.bottom_right,
|
||||||
let origin = edge_BR + Point2D::new((border.right - radius.bottom_right).max(0.),
|
&radii.bottom_right,
|
||||||
-radius.bottom_right);
|
&inner_BR,
|
||||||
let angle = if compatible_borders_corner(border.bottom, border.right) {
|
&box_BR,
|
||||||
f32::consts::FRAC_PI_4
|
&elbow_BR,
|
||||||
} else {
|
false);
|
||||||
let line = Line { start: inner_BR, end: box_BR };
|
|
||||||
let ellipse = Ellipse { origin: origin,
|
|
||||||
width: radius.bottom_right,
|
|
||||||
height: radius.bottom_right };
|
|
||||||
PaintContext::ellipse_rightmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
if distance_to_elbow_BR != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_BR, rad_B, rad_R + angle, true);
|
|
||||||
}
|
|
||||||
path_builder.arc(origin, radius.bottom_right, rad_R + angle, rad_B, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
BorderPathDrawingMode::EntireBorder => {
|
BorderPathDrawingMode::EntireBorder => {
|
||||||
path_builder.line_to(corner_BR);
|
path_builder.line_to(corner_BR);
|
||||||
|
@ -848,25 +900,13 @@ impl<'a> PaintContext<'a> {
|
||||||
}
|
}
|
||||||
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_BL),
|
BorderPathDrawingMode::CornersOnly => path_builder.move_to(corner_BL),
|
||||||
}
|
}
|
||||||
|
PaintContext::draw_corner(path_builder,
|
||||||
if radius.bottom_left != 0. {
|
&corner_origin.bottom_left,
|
||||||
let origin = edge_BL - Point2D::new((border.left - radius.bottom_left).max(0.),
|
&radii.bottom_left,
|
||||||
radius.bottom_left);
|
&inner_BL,
|
||||||
let angle = if compatible_borders_corner(border.bottom, border.left) {
|
&box_BL,
|
||||||
f32::consts::FRAC_PI_4
|
&elbow_BL,
|
||||||
} else {
|
true);
|
||||||
let line = Line { start: box_BL, end: inner_BL };
|
|
||||||
let ellipse = Ellipse { origin: origin,
|
|
||||||
width: radius.bottom_left,
|
|
||||||
height: radius.bottom_left };
|
|
||||||
PaintContext::ellipse_leftmost_line_intersection_angle(ellipse, line).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
path_builder.arc(origin, radius.bottom_left, rad_B, rad_L - angle, false);
|
|
||||||
if distance_to_elbow_BL != 0. {
|
|
||||||
path_builder.arc(origin, distance_to_elbow_BL, rad_L - angle, rad_B, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -879,6 +919,7 @@ impl<'a> PaintContext<'a> {
|
||||||
/// slice through the rounded corners. My first attempt to unify with the above code resulted
|
/// slice through the rounded corners. My first attempt to unify with the above code resulted
|
||||||
/// in making a mess of it, and the simplicity of this code path is appealing, so it may not
|
/// in making a mess of it, and the simplicity of this code path is appealing, so it may not
|
||||||
/// be worth it… In any case, revisit this decision when we support elliptical radii.
|
/// be worth it… In any case, revisit this decision when we support elliptical radii.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
fn create_rounded_rect_path(&self,
|
fn create_rounded_rect_path(&self,
|
||||||
path_builder: &mut PathBuilder,
|
path_builder: &mut PathBuilder,
|
||||||
bounds: &Rect<f32>,
|
bounds: &Rect<f32>,
|
||||||
|
@ -890,36 +931,93 @@ impl<'a> PaintContext<'a> {
|
||||||
// + 7 4 +
|
// + 7 4 +
|
||||||
// \ 6 5 /
|
// \ 6 5 /
|
||||||
// +----------+
|
// +----------+
|
||||||
|
let border = SideOffsets2D::new(radii.top_left.height.max(radii.top_right.height),
|
||||||
|
radii.bottom_right.width.max(radii.top_right.width),
|
||||||
|
radii.bottom_right.height.max(radii.bottom_left.height),
|
||||||
|
radii.top_left.height.max(radii.bottom_left.height));
|
||||||
|
|
||||||
path_builder.move_to(Point2D::new(bounds.origin.x + radii.top_left, bounds.origin.y)); // 1
|
// T = top, B = bottom, L = left, R = right
|
||||||
path_builder.line_to(Point2D::new(bounds.max_x() - radii.top_right, bounds.origin.y)); // 2
|
let inner = PaintContext::inner_border_bounds(bounds, &border);
|
||||||
path_builder.arc(Point2D::new(bounds.max_x() - radii.top_right,
|
let (outer_TL, inner_TL,
|
||||||
bounds.origin.y + radii.top_right),
|
outer_TR, inner_TR,
|
||||||
radii.top_right,
|
outer_BR, inner_BR,
|
||||||
1.5f32 * f32::consts::FRAC_PI_2,
|
outer_BL, inner_BL) = (bounds.origin, inner.origin,
|
||||||
2.0f32 * f32::consts::PI,
|
bounds.top_right(), inner.top_right(),
|
||||||
false); // 3
|
bounds.bottom_right(), inner.bottom_right(),
|
||||||
path_builder.line_to(Point2D::new(bounds.max_x(), bounds.max_y() - radii.bottom_right)); // 4
|
bounds.bottom_left(), inner.bottom_left());
|
||||||
path_builder.arc(Point2D::new(bounds.max_x() - radii.bottom_right,
|
|
||||||
bounds.max_y() - radii.bottom_right),
|
let (corner_origin, _) =
|
||||||
radii.bottom_right,
|
PaintContext::corner_bounds(bounds, &border, radii);
|
||||||
0.0,
|
let (origin_TL, origin_TR, origin_BR, origin_BL) = (corner_origin.top_left,
|
||||||
f32::consts::FRAC_PI_2,
|
corner_origin.top_right,
|
||||||
false); // 5
|
corner_origin.bottom_right,
|
||||||
path_builder.line_to(Point2D::new(bounds.origin.x + radii.bottom_left, bounds.max_y())); // 6
|
corner_origin.bottom_left);
|
||||||
path_builder.arc(Point2D::new(bounds.origin.x + radii.bottom_left,
|
let zero_elbow = Size2D::new(0., 0.);
|
||||||
bounds.max_y() - radii.bottom_left),
|
|
||||||
radii.bottom_left,
|
path_builder.move_to(Point2D::new(origin_TL.x - radii.top_left.width, origin_TL.y));
|
||||||
f32::consts::FRAC_PI_2,
|
path_builder.move_to(Point2D::new(bounds.origin.x + radii.top_left.width, bounds.origin.y)); // 1
|
||||||
f32::consts::PI,
|
path_builder.line_to(Point2D::new(bounds.max_x() - radii.top_right.width, bounds.origin.y)); // 2
|
||||||
false); // 7
|
PaintContext::draw_corner(path_builder, // 3
|
||||||
path_builder.line_to(Point2D::new(bounds.origin.x, bounds.origin.y + radii.top_left)); // 8
|
&origin_TR,
|
||||||
path_builder.arc(Point2D::new(bounds.origin.x + radii.top_left,
|
&radii.top_right,
|
||||||
bounds.origin.y + radii.top_left),
|
&inner_TR,
|
||||||
radii.top_left,
|
&outer_TR,
|
||||||
f32::consts::PI,
|
&zero_elbow,
|
||||||
1.5f32 * f32::consts::FRAC_PI_2,
|
true);
|
||||||
false); // 1
|
PaintContext::draw_corner(path_builder, // 3
|
||||||
|
&origin_TR,
|
||||||
|
&radii.top_right,
|
||||||
|
&inner_TR,
|
||||||
|
&outer_TR,
|
||||||
|
&zero_elbow,
|
||||||
|
false);
|
||||||
|
path_builder.line_to(Point2D::new(bounds.max_x(), bounds.max_y() - radii.bottom_right.width)); // 4
|
||||||
|
PaintContext::draw_corner(path_builder, // 5
|
||||||
|
&origin_BR,
|
||||||
|
&radii.bottom_right,
|
||||||
|
&inner_BR,
|
||||||
|
&outer_BR,
|
||||||
|
&zero_elbow,
|
||||||
|
true);
|
||||||
|
PaintContext::draw_corner(path_builder, // 5
|
||||||
|
&origin_BR,
|
||||||
|
&radii.bottom_right,
|
||||||
|
&inner_BR,
|
||||||
|
&outer_BR,
|
||||||
|
&zero_elbow,
|
||||||
|
false);
|
||||||
|
path_builder.line_to(Point2D::new(bounds.origin.x + radii.bottom_left.width,
|
||||||
|
bounds.max_y())); // 6
|
||||||
|
PaintContext::draw_corner(path_builder, // 7
|
||||||
|
&origin_BL,
|
||||||
|
&radii.bottom_left,
|
||||||
|
&inner_BL,
|
||||||
|
&outer_BL,
|
||||||
|
&zero_elbow,
|
||||||
|
true);
|
||||||
|
PaintContext::draw_corner(path_builder, // 7
|
||||||
|
&origin_BL,
|
||||||
|
&radii.bottom_left,
|
||||||
|
&inner_BL,
|
||||||
|
&outer_BL,
|
||||||
|
&zero_elbow,
|
||||||
|
false);
|
||||||
|
path_builder.line_to(Point2D::new(bounds.origin.x,
|
||||||
|
bounds.origin.y + radii.top_left.height)); // 8
|
||||||
|
PaintContext::draw_corner(path_builder, // 9
|
||||||
|
&origin_TL,
|
||||||
|
&radii.top_left,
|
||||||
|
&inner_TL,
|
||||||
|
&outer_TL,
|
||||||
|
&zero_elbow,
|
||||||
|
true);
|
||||||
|
PaintContext::draw_corner(path_builder, // 9
|
||||||
|
&origin_TL,
|
||||||
|
&radii.top_left,
|
||||||
|
&inner_TL,
|
||||||
|
&outer_TL,
|
||||||
|
&zero_elbow,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_dashed_border_segment(&self,
|
fn draw_dashed_border_segment(&self,
|
||||||
|
@ -947,26 +1045,26 @@ impl<'a> PaintContext<'a> {
|
||||||
let (start, end) = match direction {
|
let (start, end) = match direction {
|
||||||
Direction::Top => {
|
Direction::Top => {
|
||||||
let y = rect.origin.y + border.top * 0.5;
|
let y = rect.origin.y + border.top * 0.5;
|
||||||
let start = Point2D::new(rect.origin.x + radius.top_left, y);
|
let start = Point2D::new(rect.origin.x + radius.top_left.width, y);
|
||||||
let end = Point2D::new(rect.origin.x + rect.size.width - radius.top_right, y);
|
let end = Point2D::new(rect.origin.x + rect.size.width - radius.top_right.width, y);
|
||||||
(start, end)
|
(start, end)
|
||||||
}
|
}
|
||||||
Direction::Left => {
|
Direction::Left => {
|
||||||
let x = rect.origin.x + border.left * 0.5;
|
let x = rect.origin.x + border.left * 0.5;
|
||||||
let start = Point2D::new(x, rect.origin.y + rect.size.height - radius.bottom_left);
|
let start = Point2D::new(x, rect.origin.y + rect.size.height - radius.bottom_left.height);
|
||||||
let end = Point2D::new(x, rect.origin.y + border.top.max(radius.top_left));
|
let end = Point2D::new(x, rect.origin.y + border.top.max(radius.top_left.height));
|
||||||
(start, end)
|
(start, end)
|
||||||
}
|
}
|
||||||
Direction::Right => {
|
Direction::Right => {
|
||||||
let x = rect.origin.x + rect.size.width - border.right * 0.5;
|
let x = rect.origin.x + rect.size.width - border.right * 0.5;
|
||||||
let start = Point2D::new(x, rect.origin.y + radius.top_right);
|
let start = Point2D::new(x, rect.origin.y + radius.top_right.height);
|
||||||
let end = Point2D::new(x, rect.origin.y + rect.size.height - radius.bottom_right);
|
let end = Point2D::new(x, rect.origin.y + rect.size.height - radius.bottom_right.height);
|
||||||
(start, end)
|
(start, end)
|
||||||
}
|
}
|
||||||
Direction::Bottom => {
|
Direction::Bottom => {
|
||||||
let y = rect.origin.y + rect.size.height - border.bottom * 0.5;
|
let y = rect.origin.y + rect.size.height - border.bottom * 0.5;
|
||||||
let start = Point2D::new(rect.origin.x + rect.size.width - radius.bottom_right, y);
|
let start = Point2D::new(rect.origin.x + rect.size.width - radius.bottom_right.width, y);
|
||||||
let end = Point2D::new(rect.origin.x + border.left.max(radius.bottom_left), y);
|
let end = Point2D::new(rect.origin.x + border.left.max(radius.bottom_left.width), y);
|
||||||
(start, end)
|
(start, end)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1527,11 +1625,15 @@ impl ToRadiiPx for BorderRadii<Au> {
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderRadii {
|
BorderRadii {
|
||||||
top_left: to_nearest_px(self.top_left),
|
top_left: Size2D { width: to_nearest_px(self.top_left.width),
|
||||||
top_right: to_nearest_px(self.top_right),
|
height: to_nearest_px(self.top_left.height) },
|
||||||
bottom_left: to_nearest_px(self.bottom_left),
|
top_right: Size2D { width: to_nearest_px(self.top_right.width),
|
||||||
bottom_right: to_nearest_px(self.bottom_right),
|
height: to_nearest_px(self.top_right.height) },
|
||||||
}
|
bottom_left: Size2D { width: to_nearest_px(self.bottom_left.width),
|
||||||
|
height: to_nearest_px(self.bottom_left.height) },
|
||||||
|
bottom_right: Size2D { width: to_nearest_px(self.bottom_right.width),
|
||||||
|
height: to_nearest_px(self.bottom_right.height) },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1764,11 +1866,14 @@ enum BorderPathDrawingMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn radii_apply_to_border_direction(direction: Direction, radius: &BorderRadii<AzFloat>) -> bool {
|
fn radii_apply_to_border_direction(direction: Direction, radius: &BorderRadii<AzFloat>) -> bool {
|
||||||
match (direction, radius.top_left, radius.top_right, radius.bottom_left, radius.bottom_right) {
|
match (direction,
|
||||||
|
radius.top_left.width,
|
||||||
|
radius.top_right.width,
|
||||||
|
radius.bottom_left.width,
|
||||||
|
radius.bottom_right.width) {
|
||||||
(Direction::Top, a, b, _, _) |
|
(Direction::Top, a, b, _, _) |
|
||||||
(Direction::Right, _, a, _, b) |
|
(Direction::Right, _, a, _, b) |
|
||||||
(Direction::Bottom, _, _, a, b) |
|
(Direction::Bottom, _, _, a, b) |
|
||||||
(Direction::Left, a, _, b, _) => a != 0.0 || b != 0.0,
|
(Direction::Left, a, _, b, _) => a != 0.0 || b != 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -280,18 +280,13 @@ fn handle_overlapping_radii(size: &Size2D<Au>, radii: &BorderRadii<Au>) -> Borde
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let top_factor = scale_factor(radii.top_left, radii.top_right, size.width);
|
let top_factor = scale_factor(radii.top_left.width, radii.top_right.width, size.width);
|
||||||
let bottom_factor = scale_factor(radii.bottom_left, radii.bottom_right, size.width);
|
let bottom_factor = scale_factor(radii.bottom_left.width, radii.bottom_right.width, size.width);
|
||||||
let left_factor = scale_factor(radii.top_left, radii.bottom_left, size.height);
|
let left_factor = scale_factor(radii.top_left.height, radii.bottom_left.height, size.height);
|
||||||
let right_factor = scale_factor(radii.top_right, radii.bottom_right, size.height);
|
let right_factor = scale_factor(radii.top_right.height, radii.bottom_right.height, size.height);
|
||||||
let min_factor = top_factor.min(bottom_factor).min(left_factor).min(right_factor);
|
let min_factor = top_factor.min(bottom_factor).min(left_factor).min(right_factor);
|
||||||
if min_factor < 1.0 {
|
if min_factor < 1.0 {
|
||||||
BorderRadii {
|
radii.scale_by(min_factor)
|
||||||
top_left: radii.top_left .scale_by(min_factor),
|
|
||||||
top_right: radii.top_right .scale_by(min_factor),
|
|
||||||
bottom_left: radii.bottom_left .scale_by(min_factor),
|
|
||||||
bottom_right: radii.bottom_right.scale_by(min_factor),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
*radii
|
*radii
|
||||||
}
|
}
|
||||||
|
@ -303,14 +298,14 @@ fn build_border_radius(abs_bounds: &Rect<Au>, border_style: &Border) -> BorderRa
|
||||||
// radii will be relative to the width.
|
// radii will be relative to the width.
|
||||||
|
|
||||||
handle_overlapping_radii(&abs_bounds.size, &BorderRadii {
|
handle_overlapping_radii(&abs_bounds.size, &BorderRadii {
|
||||||
top_left: model::specified(border_style.border_top_left_radius,
|
top_left: model::specified_border_radius(border_style.border_top_left_radius,
|
||||||
abs_bounds.size.width),
|
abs_bounds.size.width),
|
||||||
top_right: model::specified(border_style.border_top_right_radius,
|
top_right: model::specified_border_radius(border_style.border_top_right_radius,
|
||||||
abs_bounds.size.width),
|
abs_bounds.size.width),
|
||||||
bottom_right: model::specified(border_style.border_bottom_right_radius,
|
bottom_right: model::specified_border_radius(border_style.border_bottom_right_radius,
|
||||||
abs_bounds.size.width),
|
abs_bounds.size.width),
|
||||||
bottom_left: model::specified(border_style.border_bottom_left_radius,
|
bottom_left: model::specified_border_radius(border_style.border_bottom_left_radius,
|
||||||
abs_bounds.size.width),
|
abs_bounds.size.width),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
|
|
||||||
use fragment::Fragment;
|
use fragment::Fragment;
|
||||||
|
|
||||||
use euclid::{Matrix4, SideOffsets2D};
|
use euclid::{Matrix4, SideOffsets2D, Size2D};
|
||||||
use std::cmp::{max, min};
|
use std::cmp::{max, min};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use style::computed_values::transform::ComputedMatrix;
|
use style::computed_values::transform::ComputedMatrix;
|
||||||
use style::properties::ComputedValues;
|
use style::properties::ComputedValues;
|
||||||
use style::values::computed::LengthOrPercentageOrAuto;
|
use style::values::computed::{BorderRadiusSize, LengthOrPercentageOrAuto};
|
||||||
use style::values::computed::{LengthOrPercentageOrNone, LengthOrPercentage};
|
use style::values::computed::{LengthOrPercentageOrNone, LengthOrPercentage};
|
||||||
use util::geometry::Au;
|
use util::geometry::Au;
|
||||||
use util::logical_geometry::LogicalMargin;
|
use util::logical_geometry::LogicalMargin;
|
||||||
|
@ -420,6 +420,13 @@ pub fn specified(length: LengthOrPercentage, containing_length: Au) -> Au {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn specified_border_radius(radius: BorderRadiusSize, containing_length: Au) -> Size2D<Au> {
|
||||||
|
let BorderRadiusSize(size) = radius;
|
||||||
|
let w = specified(size.width, containing_length);
|
||||||
|
let h = specified(size.height, containing_length);
|
||||||
|
Size2D::new(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn padding_from_style(style: &ComputedValues, containing_block_inline_size: Au)
|
pub fn padding_from_style(style: &ComputedValues, containing_block_inline_size: Au)
|
||||||
-> LogicalMargin<Au> {
|
-> LogicalMargin<Au> {
|
||||||
|
|
|
@ -333,9 +333,9 @@ pub mod longhands {
|
||||||
|
|
||||||
// FIXME(#4126): when gfx supports painting it, make this Size2D<LengthOrPercentage>
|
// FIXME(#4126): when gfx supports painting it, make this Size2D<LengthOrPercentage>
|
||||||
% for corner in ["top-left", "top-right", "bottom-right", "bottom-left"]:
|
% for corner in ["top-left", "top-right", "bottom-right", "bottom-left"]:
|
||||||
${predefined_type("border-" + corner + "-radius", "LengthOrPercentage",
|
${predefined_type("border-" + corner + "-radius", "BorderRadiusSize",
|
||||||
"computed::LengthOrPercentage::Length(Au(0))",
|
"computed::BorderRadiusSize::zero()",
|
||||||
"parse_non_negative")}
|
"parse")}
|
||||||
% endfor
|
% endfor
|
||||||
|
|
||||||
${new_style_struct("Outline", is_inherited=False)}
|
${new_style_struct("Outline", is_inherited=False)}
|
||||||
|
@ -5088,16 +5088,16 @@ pub mod shorthands {
|
||||||
'border-%s-radius' % (corner)
|
'border-%s-radius' % (corner)
|
||||||
for corner in ['top-left', 'top-right', 'bottom-right', 'bottom-left']
|
for corner in ['top-left', 'top-right', 'bottom-right', 'bottom-left']
|
||||||
)}">
|
)}">
|
||||||
use util::geometry::Au;
|
use values::specified::BorderRadiusSize;
|
||||||
use values::specified::{Length, LengthOrPercentage};
|
|
||||||
let _ignored = context;
|
let _ignored = context;
|
||||||
|
|
||||||
fn parse_one_set_of_border_radii(mut input: &mut Parser)
|
fn parse_one_set_of_border_radii(mut input: &mut Parser)
|
||||||
-> Result<[LengthOrPercentage; 4], ()> {
|
-> Result<[BorderRadiusSize; 4], ()> {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut values = [LengthOrPercentage::Length(Length::Absolute(Au(0))); 4];
|
let mut values = [BorderRadiusSize::zero(); 4];
|
||||||
while count < 4 {
|
while count < 4 {
|
||||||
if let Ok(value) = input.try(LengthOrPercentage::parse) {
|
if let Ok(value) = input.try(BorderRadiusSize::parse_one_radii) {
|
||||||
values[count] = value;
|
values[count] = value;
|
||||||
count += 1;
|
count += 1;
|
||||||
} else {
|
} else {
|
||||||
|
@ -5115,7 +5115,7 @@ pub mod shorthands {
|
||||||
}
|
}
|
||||||
|
|
||||||
let radii = try!(parse_one_set_of_border_radii(input));
|
let radii = try!(parse_one_set_of_border_radii(input));
|
||||||
// TODO(pcwalton): Elliptical borders.
|
// TODO(bjwbell): Finish parsing code for elliptical borders.
|
||||||
|
|
||||||
Ok(Longhands {
|
Ok(Longhands {
|
||||||
border_top_left_radius: Some(radii[0]),
|
border_top_left_radius: Some(radii[0]),
|
||||||
|
|
|
@ -530,6 +530,46 @@ pub mod specified {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Copy, Debug, HeapSizeOf)]
|
||||||
|
pub struct BorderRadiusSize(pub Size2D<LengthOrPercentage>);
|
||||||
|
|
||||||
|
impl BorderRadiusSize {
|
||||||
|
pub fn zero() -> BorderRadiusSize {
|
||||||
|
let zero = LengthOrPercentage::Length(Length::Absolute(Au(0)));
|
||||||
|
BorderRadiusSize(Size2D::new(zero, zero))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCss for BorderRadiusSize {
|
||||||
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
let BorderRadiusSize(size) = *self;
|
||||||
|
try!(size.width.to_css(dest));
|
||||||
|
try!(dest.write_str(" "));
|
||||||
|
size.height.to_css(dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl BorderRadiusSize {
|
||||||
|
pub fn circle(radius: LengthOrPercentage) -> BorderRadiusSize {
|
||||||
|
BorderRadiusSize(Size2D::new(radius, radius))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_one_radii(input: &mut Parser) -> Result<BorderRadiusSize, ()> {
|
||||||
|
if let Ok(first) = LengthOrPercentage::parse_non_negative(input) {
|
||||||
|
Ok(BorderRadiusSize(Size2D::new(first, first)))
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[inline]
|
||||||
|
pub fn parse(input: &mut Parser) -> Result<BorderRadiusSize, ()> {
|
||||||
|
let first = try!(LengthOrPercentage::parse_non_negative(input));
|
||||||
|
let second = input.try(LengthOrPercentage::parse_non_negative).unwrap_or(first);
|
||||||
|
Ok(BorderRadiusSize(Size2D::new(first, second)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// http://dev.w3.org/csswg/css2/colors.html#propdef-background-position
|
// http://dev.w3.org/csswg/css2/colors.html#propdef-background-position
|
||||||
#[derive(Clone, PartialEq, Copy)]
|
#[derive(Clone, PartialEq, Copy)]
|
||||||
pub enum PositionComponent {
|
pub enum PositionComponent {
|
||||||
|
@ -787,6 +827,22 @@ pub mod specified {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_border_radius(input: &mut Parser) -> Result<BorderRadiusSize, ()> {
|
||||||
|
input.try(BorderRadiusSize::parse).or_else(|()| {
|
||||||
|
match_ignore_ascii_case! { try!(input.expect_ident()),
|
||||||
|
"thin" =>
|
||||||
|
Ok(BorderRadiusSize::circle(
|
||||||
|
LengthOrPercentage::Length(Length::from_px(1.)))),
|
||||||
|
"medium" =>
|
||||||
|
Ok(BorderRadiusSize::circle(
|
||||||
|
LengthOrPercentage::Length(Length::from_px(3.)))),
|
||||||
|
"thick" =>
|
||||||
|
Ok(BorderRadiusSize::circle(
|
||||||
|
LengthOrPercentage::Length(Length::from_px(5.))))
|
||||||
|
_ => Err(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_border_width(input: &mut Parser) -> Result<Length, ()> {
|
pub fn parse_border_width(input: &mut Parser) -> Result<Length, ()> {
|
||||||
input.try(Length::parse_non_negative).or_else(|()| {
|
input.try(Length::parse_non_negative).or_else(|()| {
|
||||||
|
@ -943,6 +999,36 @@ pub mod computed {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy, HeapSizeOf)]
|
||||||
|
pub struct BorderRadiusSize(pub Size2D<LengthOrPercentage>);
|
||||||
|
|
||||||
|
impl BorderRadiusSize {
|
||||||
|
pub fn zero() -> BorderRadiusSize {
|
||||||
|
BorderRadiusSize(Size2D::new(LengthOrPercentage::Length(Au(0)), LengthOrPercentage::Length(Au(0))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToComputedValue for specified::BorderRadiusSize {
|
||||||
|
type ComputedValue = BorderRadiusSize;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn to_computed_value(&self, context: &Context) -> BorderRadiusSize {
|
||||||
|
let specified::BorderRadiusSize(s) = *self;
|
||||||
|
let w = s.width.to_computed_value(context);
|
||||||
|
let h = s.height.to_computed_value(context);
|
||||||
|
BorderRadiusSize(Size2D::new(w, h))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::cssparser::ToCss for BorderRadiusSize {
|
||||||
|
fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
|
||||||
|
let BorderRadiusSize(s) = *self;
|
||||||
|
try!(s.width.to_css(dest));
|
||||||
|
try!(dest.write_str("/"));
|
||||||
|
s.height.to_css(dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, HeapSizeOf)]
|
#[derive(PartialEq, Clone, Copy, HeapSizeOf)]
|
||||||
pub enum LengthOrPercentage {
|
pub enum LengthOrPercentage {
|
||||||
Length(Au),
|
Length(Au),
|
||||||
|
|
|
@ -64,6 +64,7 @@ flaky_cpu == append_style_a.html append_style_b.html
|
||||||
== border_radius_asymmetric_sizes_a.html border_radius_asymmetric_sizes_ref.html
|
== border_radius_asymmetric_sizes_a.html border_radius_asymmetric_sizes_ref.html
|
||||||
== border_radius_clip_a.html border_radius_clip_ref.html
|
== border_radius_clip_a.html border_radius_clip_ref.html
|
||||||
!= border_radius_dashed_a.html border_radius_dashed_ref.html
|
!= border_radius_dashed_a.html border_radius_dashed_ref.html
|
||||||
|
== border_radius_elliptical_a.html border_radius_elliptical_ref.html
|
||||||
== border_radius_overlapping_a.html border_radius_overlapping_ref.html
|
== border_radius_overlapping_a.html border_radius_overlapping_ref.html
|
||||||
== border_spacing_a.html border_spacing_ref.html
|
== border_spacing_a.html border_spacing_ref.html
|
||||||
== border_spacing_auto_layout_a.html border_spacing_ref.html
|
== border_spacing_auto_layout_a.html border_spacing_ref.html
|
||||||
|
|
25
tests/ref/border_radius_elliptical_a.html
Normal file
25
tests/ref/border_radius_elliptical_a.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta content="utf-8" http-equiv="encoding">
|
||||||
|
<style type="text/css">
|
||||||
|
div.box {
|
||||||
|
background: white;
|
||||||
|
border-width: 15px 15px 15px 15px;
|
||||||
|
border-color: yellow red green blue;
|
||||||
|
border-style: solid;
|
||||||
|
border-top-left-radius: 100px 100px;
|
||||||
|
border-top-right-radius: 200px 100px;
|
||||||
|
border-bottom-right-radius: 100px 200px;
|
||||||
|
border-bottom-left-radius: 200px 200px;
|
||||||
|
height: 500px;
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Border Radius - Elliptical</h2>
|
||||||
|
<div id="box1" class="box top"></div><br>
|
||||||
|
</body>
|
||||||
|
</html>
|
47
tests/ref/border_radius_elliptical_ref.html
Normal file
47
tests/ref/border_radius_elliptical_ref.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta content="utf-8" http-equiv="encoding">
|
||||||
|
<style type="text/css">
|
||||||
|
#box1-top {
|
||||||
|
background: white;
|
||||||
|
border-width: 15px 15px 0px 15px;
|
||||||
|
border-color: yellow red green blue;
|
||||||
|
border-style: solid;
|
||||||
|
border-top-left-radius: 100px 100px;
|
||||||
|
border-top-right-radius: 200px 100px;
|
||||||
|
height: 240px;
|
||||||
|
width: 500px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
#box1-middle {
|
||||||
|
background: white;
|
||||||
|
margin: 0px;
|
||||||
|
border-width: 0px 15px 0px 15px;
|
||||||
|
border-color: yellow red green blue;
|
||||||
|
border-style: solid;
|
||||||
|
height: 20px;
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#box1-bottom {
|
||||||
|
background: white;
|
||||||
|
border-width: 0px 15px 15px 15px;
|
||||||
|
border-color: yellow red green blue;
|
||||||
|
border-style: solid;
|
||||||
|
border-bottom-right-radius: 100px 200px;
|
||||||
|
border-bottom-left-radius: 200px 200px;
|
||||||
|
height: 240px;
|
||||||
|
width: 500px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Border Radius - Elliptical</h2>
|
||||||
|
<div id="box1-top"></div>
|
||||||
|
<div id="box1-middle"></div>
|
||||||
|
<div id="box1-bottom"></div><br>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue