use kurbo::{Arc, BezPath, Ellipse, Insets, PathEl, Point, Rect, Shape as _, Vec2};
use std::{f64::consts::FRAC_PI_2, f64::consts::PI};
use super::non_uniform_radii::NonUniformRoundedRectRadii;
use super::{Corner, CssBoxKind, Direction, Edge, add_insets, get_corner_insets};
#[derive(Debug, Clone)]
pub struct CssBox {
pub border_box: Rect,
pub padding_box: Rect,
pub content_box: Rect,
pub outline_box: Rect,
pub padding_width: Insets,
pub border_width: Insets,
pub outline_width: f64,
pub border_radii: NonUniformRoundedRectRadii,
}
impl CssBox {
pub fn new(
border_box: Rect,
border: Insets,
padding: Insets,
outline_width: f64,
mut border_radii: NonUniformRoundedRectRadii,
) -> Self {
let padding_box = border_box - border;
let content_box = padding_box - padding;
let outline_box = border_box.inset(outline_width);
let top_overlap_factor =
border_box.width() / (border_radii.top_left.x + border_radii.top_right.x);
let bottom_overlap_factor =
border_box.width() / (border_radii.bottom_left.x + border_radii.bottom_right.x);
let left_overlap_factor =
border_box.height() / (border_radii.top_left.y + border_radii.bottom_left.y);
let right_overlap_factor =
border_box.height() / (border_radii.top_right.y + border_radii.bottom_right.y);
let min_factor = top_overlap_factor
.min(bottom_overlap_factor)
.min(left_overlap_factor)
.min(right_overlap_factor)
.min(1.0);
if min_factor < 1.0 {
border_radii *= min_factor
}
Self {
padding_box,
border_box,
content_box,
outline_box,
outline_width,
padding_width: padding,
border_width: border,
border_radii,
}
}
pub fn border_edge_shape(&self, edge: Edge) -> BezPath {
use {Corner::*, CssBoxKind::*, Direction::*, Edge::*};
let mut path = BezPath::new();
let (c0, c1) = match edge {
Top => (TopLeft, TopRight),
Right => (TopRight, BottomRight),
Bottom => (BottomRight, BottomLeft),
Left => (BottomLeft, TopLeft),
};
if self.is_sharp(c0, BorderBox) {
path.move_to(self.corner(c0, PaddingBox));
path.line_to(self.corner(c0, BorderBox));
} else {
match self.corner_needs_infill(c0) {
true => {
path.insert_arc(self.partial_corner_arc(c0, PaddingBox, edge, Anticlockwise))
}
false => path.move_to(self.corner(c0, PaddingBox)),
}
path.insert_arc(self.partial_corner_arc(c0, BorderBox, edge, Clockwise));
}
if self.is_sharp(c1, BorderBox) {
path.line_to(self.corner(c1, BorderBox));
path.line_to(self.corner(c1, PaddingBox));
} else {
path.insert_arc(self.partial_corner_arc(c1, BorderBox, edge, Clockwise));
match self.corner_needs_infill(c1) {
true => {
path.insert_arc(self.partial_corner_arc(c1, PaddingBox, edge, Anticlockwise))
}
false => path.line_to(self.corner(c1, PaddingBox)),
}
}
path
}
pub fn outline(&self) -> BezPath {
let mut path = BezPath::new();
self.shape(&mut path, CssBoxKind::OutlineBox, Direction::Clockwise);
path.move_to(self.corner(Corner::TopLeft, CssBoxKind::BorderBox));
self.shape(&mut path, CssBoxKind::BorderBox, Direction::Anticlockwise);
path.move_to(self.corner(Corner::TopLeft, CssBoxKind::BorderBox));
path
}
pub fn border_box_path(&self) -> BezPath {
let mut path = BezPath::new();
self.shape(&mut path, CssBoxKind::BorderBox, Direction::Clockwise);
path
}
pub fn padding_box_path(&self) -> BezPath {
let mut path = BezPath::new();
self.shape(&mut path, CssBoxKind::PaddingBox, Direction::Clockwise);
path
}
pub fn content_box_path(&self) -> BezPath {
let mut path = BezPath::new();
self.shape(&mut path, CssBoxKind::ContentBox, Direction::Clockwise);
path
}
fn shape(&self, path: &mut BezPath, line: CssBoxKind, direction: Direction) {
use Corner::*;
let route = match direction {
Direction::Clockwise => [TopLeft, TopRight, BottomRight, BottomLeft],
Direction::Anticlockwise => [TopLeft, BottomLeft, BottomRight, TopRight],
};
for corner in route {
if self.is_sharp(corner, line) {
path.insert_point(self.corner(corner, line));
} else {
path.insert_arc(self.corner_arc(corner, line, direction));
}
}
}
pub fn shadow_clip(&self, shadow_rect: Rect) -> BezPath {
let mut path = BezPath::new();
self.shadow_clip_shape(&mut path, shadow_rect);
path
}
fn shadow_clip_shape(&self, path: &mut BezPath, shadow_rect: Rect) {
use Corner::*;
for corner in [TopLeft, TopRight, BottomRight, BottomLeft] {
path.insert_point(self.shadow_clip_corner(corner, shadow_rect));
}
if self.is_sharp(TopLeft, CssBoxKind::BorderBox) {
path.move_to(self.corner(TopLeft, CssBoxKind::BorderBox));
} else {
const TOLERANCE: f64 = 0.1;
let arc = self.corner_arc(TopLeft, CssBoxKind::BorderBox, Direction::Anticlockwise);
let elements = arc.path_elements(TOLERANCE);
path.extend(elements);
}
for corner in [ BottomLeft, BottomRight, TopRight] {
if self.is_sharp(corner, CssBoxKind::BorderBox) {
path.insert_point(self.corner(corner, CssBoxKind::BorderBox));
} else {
path.insert_arc(self.corner_arc(
corner,
CssBoxKind::BorderBox,
Direction::Anticlockwise,
));
}
}
}
fn corner(&self, corner: Corner, css_box: CssBoxKind) -> Point {
let Rect { x0, y0, x1, y1 } = match css_box {
CssBoxKind::OutlineBox => self.outline_box,
CssBoxKind::BorderBox => self.border_box,
CssBoxKind::PaddingBox => self.padding_box,
CssBoxKind::ContentBox => self.content_box,
};
match corner {
Corner::TopLeft => Point { x: x0, y: y0 },
Corner::TopRight => Point { x: x1, y: y0 },
Corner::BottomLeft => Point { x: x0, y: y1 },
Corner::BottomRight => Point { x: x1, y: y1 },
}
}
fn shadow_clip_corner(&self, corner: Corner, shadow_rect: Rect) -> Point {
let (x, y) = match corner {
Corner::TopLeft => (shadow_rect.x0, shadow_rect.y0),
Corner::TopRight => (shadow_rect.x1, shadow_rect.y0),
Corner::BottomRight => (shadow_rect.x1, shadow_rect.y1),
Corner::BottomLeft => (shadow_rect.x0, shadow_rect.y1),
};
Point { x, y }
}
fn corner_needs_infill(&self, corner: Corner) -> bool {
match corner {
Corner::TopLeft => {
self.border_radii.top_left.x > self.border_width.x0
&& self.border_radii.top_left.y > self.border_width.y0
}
Corner::TopRight => {
self.border_radii.top_right.x > self.border_width.x1
&& self.border_radii.top_right.y > self.border_width.y0
}
Corner::BottomRight => {
self.border_radii.bottom_right.x > self.border_width.x1
&& self.border_radii.bottom_right.y > self.border_width.y1
}
Corner::BottomLeft => {
self.border_radii.bottom_left.x > self.border_width.x0
&& self.border_radii.bottom_left.y > self.border_width.y1
}
}
}
fn corner_arc(&self, corner: Corner, css_box: CssBoxKind, direction: Direction) -> Arc {
let ellipse = self.ellipse(corner, css_box);
let sweep_direction = match direction {
Direction::Anticlockwise => -1.0,
Direction::Clockwise => 1.0,
};
let offset = match corner {
Corner::TopLeft => -FRAC_PI_2,
Corner::TopRight => 0.0,
Corner::BottomRight => FRAC_PI_2,
Corner::BottomLeft => PI,
};
let offset = match direction {
Direction::Clockwise => offset,
Direction::Anticlockwise => offset + FRAC_PI_2,
};
Arc::new(
ellipse.center(),
ellipse.radii(),
offset + PI + FRAC_PI_2,
FRAC_PI_2 * sweep_direction,
0.0,
)
}
fn partial_corner_arc(
&self,
corner: Corner,
css_box: CssBoxKind,
edge: Edge,
direction: Direction,
) -> Arc {
use Corner::*;
use CssBoxKind::*;
use Edge::*;
let ellipse = self.ellipse(corner, css_box);
let theta = self.start_angle(corner, ellipse.radii());
let sweep_direction = match direction {
Direction::Anticlockwise => -1.0,
Direction::Clockwise => 1.0,
};
let offset = match edge {
Top => 0.0,
Right => FRAC_PI_2,
Bottom => PI,
Left => PI + FRAC_PI_2,
};
let theta = match edge {
Top | Bottom => FRAC_PI_2 - theta,
Right | Left => theta,
};
let start = match (edge, corner, css_box) {
(Top, TopLeft, PaddingBox) => 0.0,
(Top, TopLeft, BorderBox) => -theta,
(Top, TopRight, BorderBox) => 0.0,
(Top, TopRight, PaddingBox) => theta,
(Right, TopRight, PaddingBox) => 0.0,
(Right, TopRight, BorderBox) => -theta,
(Right, BottomRight, BorderBox) => 0.0,
(Right, BottomRight, PaddingBox) => theta,
(Bottom, BottomRight, PaddingBox) => 0.0,
(Bottom, BottomRight, BorderBox) => -theta,
(Bottom, BottomLeft, BorderBox) => 0.0,
(Bottom, BottomLeft, PaddingBox) => theta,
(Left, BottomLeft, PaddingBox) => 0.0,
(Left, BottomLeft, BorderBox) => -theta,
(Left, TopLeft, BorderBox) => 0.0,
(Left, TopLeft, PaddingBox) => theta,
_ => unreachable!("Invalid edge/corner combination"),
};
Arc::new(
ellipse.center(),
ellipse.radii(),
start + offset + PI + FRAC_PI_2,
theta * sweep_direction,
0.0,
)
}
fn is_sharp(&self, corner: Corner, side: CssBoxKind) -> bool {
use Corner::*;
use CssBoxKind::*;
let corner_radii = match corner {
TopLeft => self.border_radii.top_left,
TopRight => self.border_radii.top_right,
BottomLeft => self.border_radii.bottom_left,
BottomRight => self.border_radii.bottom_right,
};
let is_sharp = (corner_radii.x == 0.0) | (corner_radii.y == 0.0);
if is_sharp {
return true;
}
let css_box: Insets = match side {
OutlineBox => return false,
BorderBox => return false,
PaddingBox => self.border_width,
ContentBox => add_insets(self.border_width, self.padding_width),
};
match corner {
TopLeft => (corner_radii.x <= css_box.x0) | (corner_radii.y <= css_box.y0),
TopRight => (corner_radii.x <= css_box.x1) | (corner_radii.y <= css_box.y0),
BottomLeft => (corner_radii.x <= css_box.x0) | (corner_radii.y <= css_box.y1),
BottomRight => (corner_radii.x <= css_box.x1) | (corner_radii.y <= css_box.y1),
}
}
fn ellipse(&self, corner: Corner, side: CssBoxKind) -> Ellipse {
use {Corner::*, CssBoxKind::*};
let CssBox {
border_box,
padding_width,
border_width,
border_radii,
..
} = self;
let corner_radii = match corner {
TopLeft => border_radii.top_left,
TopRight => border_radii.top_right,
BottomLeft => border_radii.bottom_left,
BottomRight => border_radii.bottom_right,
};
let center = match corner {
TopLeft => corner_radii,
TopRight => Vec2 {
x: border_box.width() - corner_radii.x,
y: corner_radii.y,
},
BottomLeft => Vec2 {
x: corner_radii.x,
y: border_box.height() - corner_radii.y,
},
BottomRight => Vec2 {
x: border_box.width() - corner_radii.x,
y: border_box.height() - corner_radii.y,
},
};
let radii: Vec2 = match side {
BorderBox => corner_radii,
OutlineBox => corner_radii + Vec2::new(self.outline_width, self.outline_width),
PaddingBox => corner_radii - get_corner_insets(*border_width, corner),
ContentBox => {
corner_radii - get_corner_insets(add_insets(*border_width, *padding_width), corner)
}
};
Ellipse::new(border_box.origin() + center, radii, 0.0)
}
fn start_angle(&self, corner: Corner, radii: Vec2) -> f64 {
let corner_insets = get_corner_insets(self.border_width, corner);
start_angle(corner_insets.y, corner_insets.x, radii)
}
}
trait BuildBezpath {
const TOLERANCE: f64;
fn insert_arc(&mut self, arc: Arc);
fn insert_point(&mut self, point: Point);
}
impl BuildBezpath for BezPath {
const TOLERANCE: f64 = 0.1;
fn insert_arc(&mut self, arc: Arc) {
let mut elements = arc.path_elements(Self::TOLERANCE);
match elements.next().unwrap() {
PathEl::MoveTo(a) if !self.elements().is_empty() => self.push(PathEl::LineTo(a)),
el => self.push(el),
}
self.extend(elements)
}
fn insert_point(&mut self, point: Point) {
if self.elements().is_empty() {
self.push(PathEl::MoveTo(point));
} else {
self.push(PathEl::LineTo(point));
}
}
}
fn start_angle(bt_width: f64, br_width: f64, radii: Vec2) -> f64 {
let w = bt_width / br_width;
let x = radii.y / (w * radii.x);
use std::f64::consts::SQRT_2;
let numerator: f64 = x - x.sqrt() * SQRT_2;
let denonimantor: f64 = x - 2.0;
(numerator / denonimantor).atan() * 2.0
}
#[test]
fn should_solve_properly() {
dbg!(start_angle(4.0, 1.0, Vec2 { x: 1.0, y: 2.0 }));
}