Add round corners and change arguments

This commit is contained in:
Martin Haug 2022-04-30 21:59:34 +02:00
parent f9e115daf5
commit 5f1499d380
9 changed files with 473 additions and 96 deletions

View File

@ -16,9 +16,9 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset; use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore}; use crate::font::{find_name, FaceId, FontStore};
use crate::frame::{Element, Frame, Geometry, Group, Shape, Text}; use crate::frame::{rect_path, rect_paths, Element, Frame, Geometry, Group, Shape, Text};
use crate::geom::{ use crate::geom::{
self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform, self, Color, Em, Length, Numeric, Paint, Point, Sides, Size, Stroke, Transform,
}; };
use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::Context; use crate::Context;
@ -499,16 +499,16 @@ impl<'a> PageExporter<'a> {
} }
fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) { fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) {
if shape.fill.is_none() && shape.stroke.is_none() { if shape.fill.is_none() && shape.stroke.iter().all(Option::is_none) {
return; return;
} }
match shape.geometry { match shape.geometry {
Geometry::Rect(size) => { Geometry::Rect(size, radius) => {
let w = size.x.to_f32(); let w = size.x.to_f32();
let h = size.y.to_f32(); let h = size.y.to_f32();
if w > 0.0 && h > 0.0 { if w > 0.0 && h > 0.0 {
self.content.rect(x, y, w, h); self.write_path(x, y, &rect_path(size, radius));
} }
} }
Geometry::Ellipse(size) => { Geometry::Ellipse(size) => {
@ -530,16 +530,37 @@ impl<'a> PageExporter<'a> {
self.set_fill(fill); self.set_fill(fill);
} }
if let Some(stroke) = shape.stroke { // The stroke does not exist or is non-uniform.
let mut use_stroke = false;
if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) {
if let Some(stroke) = shape.stroke.top {
self.set_stroke(stroke); self.set_stroke(stroke);
use_stroke = true;
}
} }
match (shape.fill, shape.stroke) { match (shape.fill, use_stroke) {
(None, None) => unreachable!(), (None, false) => self.content.end_path(),
(Some(_), None) => self.content.fill_nonzero(), (Some(_), false) => self.content.fill_nonzero(),
(None, Some(_)) => self.content.stroke(), (None, true) => self.content.stroke(),
(Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(), (Some(_), true) => self.content.fill_nonzero_and_stroke(),
}; };
if let Geometry::Rect(size, radius) = shape.geometry {
if !use_stroke {
for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) {
if let Some(stroke) = stroke {
self.write_shape(x, y, &Shape {
geometry: Geometry::Path(path),
fill: None,
stroke: Sides::splat(Some(stroke)),
});
} else {
continue;
}
}
}
}
} }
fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) { fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) {

View File

@ -8,8 +8,9 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use usvg::FitTo; use usvg::FitTo;
use crate::frame::{Element, Frame, Geometry, Group, Shape, Text}; use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
use crate::geom::{self, Length, Paint, PathElement, Size, Stroke, Transform}; use crate::geom::{self, Length, Paint, PathElement, Sides, Size, Stroke, Transform};
use crate::image::{Image, RasterImage, Svg}; use crate::image::{Image, RasterImage, Svg};
use crate::library::prelude::{rect_path, rect_paths};
use crate::Context; use crate::Context;
/// Export a frame into a rendered image. /// Export a frame into a rendered image.
@ -298,12 +299,7 @@ fn render_shape(
shape: &Shape, shape: &Shape,
) -> Option<()> { ) -> Option<()> {
let path = match shape.geometry { let path = match shape.geometry {
Geometry::Rect(size) => { Geometry::Rect(size, radius) => convert_path(&rect_path(size, radius))?,
let w = size.x.to_f32();
let h = size.y.to_f32();
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?;
sk::PathBuilder::from_rect(rect)
}
Geometry::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?, Geometry::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?,
Geometry::Line(target) => { Geometry::Line(target) => {
let mut builder = sk::PathBuilder::new(); let mut builder = sk::PathBuilder::new();
@ -315,7 +311,7 @@ fn render_shape(
if let Some(fill) = shape.fill { if let Some(fill) = shape.fill {
let mut paint: sk::Paint = fill.into(); let mut paint: sk::Paint = fill.into();
if matches!(shape.geometry, Geometry::Rect(_)) { if matches!(shape.geometry, Geometry::Rect(_, _)) {
paint.anti_alias = false; paint.anti_alias = false;
} }
@ -323,12 +319,28 @@ fn render_shape(
canvas.fill_path(&path, &paint, rule, ts, mask); canvas.fill_path(&path, &paint, rule, ts, mask);
} }
if let Some(Stroke { paint, thickness }) = shape.stroke { if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) {
if let Some(Stroke { paint, thickness }) = shape.stroke.top {
let paint = paint.into(); let paint = paint.into();
let mut stroke = sk::Stroke::default(); let mut stroke = sk::Stroke::default();
stroke.width = thickness.to_f32(); stroke.width = thickness.to_f32();
canvas.stroke_path(&path, &paint, &stroke, ts, mask); canvas.stroke_path(&path, &paint, &stroke, ts, mask);
} }
} else {
if let Geometry::Rect(size, radius) = shape.geometry {
for (path, stroke) in rect_paths(size, radius, Some(shape.stroke)) {
if let Some(stroke) = stroke {
render_shape(canvas, ts, mask, &Shape {
geometry: Geometry::Path(path),
fill: None,
stroke: Sides::splat(Some(stroke)),
})?;
} else {
continue;
}
}
}
}
Some(()) Some(())
} }

View File

@ -1,11 +1,13 @@
//! Finished layouts. //! Finished layouts.
use std::fmt::{self, Debug, Formatter, Write}; use std::fmt::{self, Debug, Formatter, Write};
use std::mem;
use std::sync::Arc; use std::sync::Arc;
use crate::font::FaceId; use crate::font::FaceId;
use crate::geom::{ use crate::geom::{
Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform, Align, Angle, Em, Get, Length, Numeric, Paint, Path, Point, Side, Sides, Size, Spec,
Stroke, Transform,
}; };
use crate::image::ImageId; use crate::image::ImageId;
use crate::util::{EcoString, MaybeShared}; use crate::util::{EcoString, MaybeShared};
@ -306,7 +308,7 @@ pub struct Shape {
/// The shape's background fill. /// The shape's background fill.
pub fill: Option<Paint>, pub fill: Option<Paint>,
/// The shape's border stroke. /// The shape's border stroke.
pub stroke: Option<Stroke>, pub stroke: Sides<Option<Stroke>>,
} }
/// A shape's geometry. /// A shape's geometry.
@ -314,8 +316,8 @@ pub struct Shape {
pub enum Geometry { pub enum Geometry {
/// A line to a point (relative to its position). /// A line to a point (relative to its position).
Line(Point), Line(Point),
/// A rectangle with its origin in the topleft corner. /// A rectangle with its origin in the topleft corner and a border radius.
Rect(Size), Rect(Size, Sides<Length>),
/// A ellipse with its origin in the topleft corner. /// A ellipse with its origin in the topleft corner.
Ellipse(Size), Ellipse(Size),
/// A bezier path. /// A bezier path.
@ -328,7 +330,7 @@ impl Geometry {
Shape { Shape {
geometry: self, geometry: self,
fill: Some(fill), fill: Some(fill),
stroke: None, stroke: Sides::splat(None),
} }
} }
@ -337,7 +339,170 @@ impl Geometry {
Shape { Shape {
geometry: self, geometry: self,
fill: None, fill: None,
stroke: Some(stroke), stroke: Sides::splat(Some(stroke)),
} }
} }
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Connection {
None,
Left,
Right,
Both,
}
impl Connection {
pub fn advance(self, right: bool) -> Self {
match self {
Self::Right | Self::Both => {
if right {
Self::Both
} else {
Self::Left
}
}
Self::Left | Self::None => {
if right {
Self::Right
} else {
Self::None
}
}
}
}
fn left(self) -> bool {
matches!(self, Self::Left | Self::Both)
}
fn right(self) -> bool {
matches!(self, Self::Right | Self::Both)
}
}
/// Draws one side of the rounded rectangle. Will always draw the left arc. The
/// right arc will be drawn halfway iff there is no connection.
fn draw_side(
path: &mut Path,
side: Side,
size: Size,
radius_left: Length,
radius_right: Length,
connection: Connection,
) {
let reversed = |angle: Angle, radius, rotate, mirror_x, mirror_y| {
let [a, b, c, d] = angle.bezier_arc(radius, rotate, mirror_x, mirror_y);
[d, c, b, a]
};
let angle_left = Angle::deg(if connection.left() { 90.0 } else { 45.0 });
let angle_right = Angle::deg(if connection.right() { 90.0 } else { 45.0 });
let (arc1, arc2) = match side {
Side::Top => {
let arc1 = reversed(angle_left, radius_left, true, true, false)
.map(|x| x + Point::with_x(radius_left));
let arc2 = (-angle_right)
.bezier_arc(radius_right, true, true, false)
.map(|x| x + Point::with_x(size.x - radius_right));
(arc1, arc2)
}
Side::Right => {
let arc1 = reversed(-angle_left, radius_left, false, false, false)
.map(|x| x + Point::new(size.x, radius_left));
let arc2 = angle_right
.bezier_arc(radius_right, false, false, false)
.map(|x| x + Point::new(size.x, size.y - radius_right));
(arc1, arc2)
}
Side::Bottom => {
let arc1 = reversed(-angle_left, radius_left, true, false, false)
.map(|x| x + Point::new(size.x - radius_left, size.y));
let arc2 = angle_right
.bezier_arc(radius_right, true, false, false)
.map(|x| x + Point::new(radius_right, size.y));
(arc1, arc2)
}
Side::Left => {
let arc1 = reversed(angle_left, radius_left, false, false, true)
.map(|x| x + Point::with_y(size.y - radius_left));
let arc2 = (-angle_right)
.bezier_arc(radius_right, false, false, true)
.map(|x| x + Point::with_y(radius_right));
(arc1, arc2)
}
};
if !connection.left() {
path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] });
}
if !radius_left.is_zero() {
path.cubic_to(arc1[1], arc1[2], arc1[3]);
}
path.line_to(arc2[0]);
if !connection.right() && !radius_right.is_zero() {
path.cubic_to(arc2[1], arc2[2], arc2[3]);
}
}
pub fn rect_paths(
size: Size,
radius: Sides<Length>,
strokes: Option<Sides<Option<Stroke>>>,
) -> Vec<(Path, Option<Stroke>)> {
let strokes = strokes.unwrap_or_else(|| Sides::splat(None));
let mut res = vec![];
let mut connection = Connection::None;
let mut path = Path::new();
let sides = [Side::Top, Side::Right, Side::Bottom, Side::Left];
let mut always_continuous = true;
let radius = [
radius.left,
radius.top,
radius.right,
radius.bottom,
radius.left,
];
for (side, radius) in sides.into_iter().zip(radius.windows(2)) {
let stroke_continuity = strokes.get(side) == strokes.get(side.clockwise());
connection = connection.advance(stroke_continuity && side != Side::Left);
always_continuous &= stroke_continuity;
draw_side(&mut path, side, size, radius[0], radius[1], connection);
if !stroke_continuity {
res.push((mem::take(&mut path), strokes.get(side)));
}
}
if always_continuous {
path.close_path();
}
if !path.0.is_empty() {
res.push((path, strokes.left));
}
res
}
pub fn rect_path(size: Size, radius: Sides<Length>) -> Path {
let mut paths = rect_paths(size, radius, None);
assert_eq!(paths.len(), 1);
paths.pop().unwrap().0
}

View File

@ -64,6 +64,51 @@ impl Angle {
pub fn cos(self) -> f64 { pub fn cos(self) -> f64 {
self.to_rad().cos() self.to_rad().cos()
} }
/// Get the control points for a bezier curve that describes a circular arc
/// of this angle with the given radius.
pub fn bezier_arc(
self,
radius: Length,
rotate: bool,
mirror_x: bool,
mirror_y: bool,
) -> [Point; 4] {
let end = Point::new(self.cos() * radius - radius, self.sin() * radius);
let center = Point::new(-radius, Length::zero());
let mut ts = if mirror_y {
Transform::mirror_y()
} else {
Transform::identity()
};
if mirror_x {
ts = ts.pre_concat(Transform::mirror_x());
}
if rotate {
ts = ts.pre_concat(Transform::rotate(Angle::deg(90.0)));
}
let a = center * -1.0;
let b = end - center;
let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw();
let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw();
let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2)
/ (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw());
let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x);
let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x);
[
Point::zero(),
control_1.transform(ts),
control_2.transform(ts),
end.transform(ts),
]
}
} }
impl Numeric for Angle { impl Numeric for Angle {

View File

@ -31,6 +31,32 @@ impl<T> Sides<T> {
bottom: value, bottom: value,
} }
} }
/// Maps the individual fields with `f`.
pub fn map<F, U>(self, mut f: F) -> Sides<U>
where
F: FnMut(T) -> U,
{
Sides {
left: f(self.left),
top: f(self.top),
right: f(self.right),
bottom: f(self.bottom),
}
}
/// Returns an iterator over the sides.
pub fn iter(&self) -> impl Iterator<Item = &T> {
[&self.left, &self.top, &self.right, &self.bottom].into_iter()
}
/// Returns whether all sides are equal.
pub fn is_uniform(&self) -> bool
where
T: PartialEq,
{
self.left == self.top && self.top == self.right && self.right == self.bottom
}
} }
impl<T> Sides<T> impl<T> Sides<T>
@ -100,4 +126,24 @@ impl Side {
Self::Bottom => Self::Top, Self::Bottom => Self::Top,
} }
} }
/// The next side, clockwise.
pub fn clockwise(self) -> Self {
match self {
Self::Left => Self::Top,
Self::Top => Self::Right,
Self::Right => Self::Bottom,
Self::Bottom => Self::Left,
}
}
/// The next side, counter-clockwise.
pub fn counter_clockwise(self) -> Self {
match self {
Self::Left => Self::Bottom,
Self::Top => Self::Left,
Self::Right => Self::Top,
Self::Bottom => Self::Right,
}
}
} }

View File

@ -24,6 +24,30 @@ impl Transform {
} }
} }
/// Transform by mirroring along the x-axis.
pub fn mirror_x() -> Self {
Self {
sx: Ratio::one(),
ky: Ratio::zero(),
kx: Ratio::zero(),
sy: -Ratio::one(),
tx: Length::zero(),
ty: Length::zero(),
}
}
/// Transform by mirroring along the y-axis.
pub fn mirror_y() -> Self {
Self {
sx: -Ratio::one(),
ky: Ratio::zero(),
kx: Ratio::zero(),
sy: Ratio::one(),
tx: Length::zero(),
ty: Length::zero(),
}
}
/// A translate transform. /// A translate transform.
pub const fn translate(tx: Length, ty: Length) -> Self { pub const fn translate(tx: Length, ty: Length) -> Self {
Self { tx, ty, ..Self::identity() } Self { tx, ty, ..Self::identity() }

View File

@ -5,36 +5,46 @@ use crate::library::text::TextNode;
/// Place a node into a sizable and fillable shape. /// Place a node into a sizable and fillable shape.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct ShapeNode<const S: ShapeKind>(pub Option<LayoutNode>); pub struct AngularNode<const S: ShapeKind>(pub Option<LayoutNode>);
/// Place a node into a square. /// Place a node into a square.
pub type SquareNode = ShapeNode<SQUARE>; pub type SquareNode = AngularNode<SQUARE>;
/// Place a node into a rectangle. /// Place a node into a rectangle.
pub type RectNode = ShapeNode<RECT>; pub type RectNode = AngularNode<RECT>;
/// Place a node into a circle. // /// Place a node into a sizable and fillable shape.
pub type CircleNode = ShapeNode<CIRCLE>; // #[derive(Debug, Hash)]
// pub struct RoundNode<const S: ShapeKind>(pub Option<LayoutNode>);
/// Place a node into an ellipse. // /// Place a node into a circle.
pub type EllipseNode = ShapeNode<ELLIPSE>; // pub type CircleNode = RoundNode<CIRCLE>;
// /// Place a node into an ellipse.
// pub type EllipseNode = RoundNode<ELLIPSE>;
#[node] #[node]
impl<const S: ShapeKind> ShapeNode<S> { impl<const S: ShapeKind> AngularNode<S> {
/// How to fill the shape. /// How to fill the shape.
pub const FILL: Option<Paint> = None; pub const FILL: Option<Paint> = None;
/// How to stroke the shape. /// How to stroke the shape.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto; pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto;
/// How much to pad the shape's content. /// How much to pad the shape's content.
pub const PADDING: Relative<RawLength> = Relative::zero(); #[property(resolve, fold)]
pub const INSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to extend the shape's dimensions beyond the allocated space.
#[property(resolve, fold)]
pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
/// How much to round the shape's corners.
#[property(resolve, fold)]
pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let size = match S { let size = args.named::<RawLength>("size")?.map(Relative::from);
SQUARE => args.named::<RawLength>("size")?.map(Relative::from),
CIRCLE => args.named::<RawLength>("radius")?.map(|r| 2.0 * Relative::from(r)),
_ => None,
};
let width = match size { let width = match size {
None => args.named("width")?, None => args.named("width")?,
@ -52,7 +62,50 @@ impl<const S: ShapeKind> ShapeNode<S> {
} }
} }
impl<const S: ShapeKind> Layout for ShapeNode<S> { castable! {
Sides<Option<RawStroke>>,
Expected: "stroke, dictionary with strokes for each side",
Value::None => {
Sides::splat(None)
},
Value::Dict(values) => {
let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None);
Sides {
top: get("top"),
right: get("right"),
bottom: get("bottom"),
left: get("left"),
}
},
Value::Length(thickness) => Sides::splat(Some(RawStroke {
paint: Smart::Auto,
thickness: Smart::Custom(thickness),
})),
Value::Color(color) => Sides::splat(Some(RawStroke {
paint: Smart::Custom(color.into()),
thickness: Smart::Auto,
})),
@stroke: RawStroke => Sides::splat(Some(*stroke)),
}
castable! {
Sides<Option<Relative<RawLength>>>,
Expected: "length or dictionary of lengths for each side",
Value::None => Sides::splat(None),
Value::Dict(values) => {
let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None);
Sides {
top: get("top"),
right: get("right"),
bottom: get("bottom"),
left: get("left"),
}
},
Value::Length(l) => Sides::splat(Some(l.into())),
Value::Relative(r) => Sides::splat(Some(r)),
}
impl<const S: ShapeKind> Layout for AngularNode<S> {
fn layout( fn layout(
&self, &self,
ctx: &mut Context, ctx: &mut Context,
@ -61,20 +114,18 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let mut frames; let mut frames;
if let Some(child) = &self.0 { if let Some(child) = &self.0 {
let mut padding = styles.get(Self::PADDING); let inset = styles.get(Self::INSET);
if is_round(S) {
padding.rel += Ratio::new(0.5 - SQRT_2 / 4.0);
}
// Pad the child. // Pad the child.
let child = child.clone().padded(Sides::splat(padding)); let child = child
.clone()
.padded(inset.map(|side| side.map(|abs| RawLength::from(abs))));
let mut pod = Regions::one(regions.first, regions.base, regions.expand); let mut pod = Regions::one(regions.first, regions.base, regions.expand);
frames = child.layout(ctx, &pod, styles)?; frames = child.layout(ctx, &pod, styles)?;
// Relayout with full expansion into square region to make sure // Relayout with full expansion into square region to make sure
// the result is really a square or circle. // the result is really a square or circle.
if is_quadratic(S) {
let length = if regions.expand.x || regions.expand.y { let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.first, Size::zero()); let target = regions.expand.select(regions.first, Size::zero());
target.x.max(target.y) target.x.max(target.y)
@ -87,14 +138,12 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
pod.first = Size::splat(length); pod.first = Size::splat(length);
pod.expand = Spec::splat(true); pod.expand = Spec::splat(true);
frames = child.layout(ctx, &pod, styles)?; frames = child.layout(ctx, &pod, styles)?;
}
} else { } else {
// The default size that a shape takes on if it has no child and // The default size that a shape takes on if it has no child and
// enough space. // enough space.
let mut size = let mut size =
Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.first); Size::new(Length::pt(45.0), Length::pt(30.0)).min(regions.first);
if is_quadratic(S) {
let length = if regions.expand.x || regions.expand.y { let length = if regions.expand.x || regions.expand.y {
let target = regions.expand.select(regions.first, Size::zero()); let target = regions.expand.select(regions.first, Size::zero());
target.x.max(target.y) target.x.max(target.y)
@ -102,9 +151,6 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
size.x.min(size.y) size.x.min(size.y)
}; };
size = Size::splat(length); size = Size::splat(length);
} else {
size = regions.expand.select(regions.first, size);
}
frames = vec![Arc::new(Frame::new(size))]; frames = vec![Arc::new(Frame::new(size))];
} }
@ -114,18 +160,28 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
// Add fill and/or stroke. // Add fill and/or stroke.
let fill = styles.get(Self::FILL); let fill = styles.get(Self::FILL);
let stroke = match styles.get(Self::STROKE) { let stroke = match styles.get(Self::STROKE) {
Smart::Auto => fill.is_none().then(Stroke::default), Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default), Smart::Auto => Sides::splat(None),
Smart::Custom(strokes) => strokes.map(|s| Some(s.unwrap_or_default())),
}; };
if fill.is_some() || stroke.is_some() { let radius = {
let geometry = if is_round(S) { let radius = styles.get(Self::RADIUS);
Geometry::Ellipse(frame.size)
} else { Sides {
Geometry::Rect(frame.size) left: radius.left.relative_to(frame.size.x / 2.0),
top: radius.top.relative_to(frame.size.y / 2.0),
right: radius.right.relative_to(frame.size.x / 2.0),
bottom: radius.bottom.relative_to(frame.size.y / 2.0),
}
}; };
let shape = Shape { geometry, fill, stroke }; if fill.is_some() || stroke.iter().any(Option::is_some) {
let shape = Shape {
geometry: Geometry::Rect(frame.size, radius),
fill,
stroke,
};
frame.prepend(Point::zero(), Element::Shape(shape)); frame.prepend(Point::zero(), Element::Shape(shape));
} }
@ -152,13 +208,3 @@ const CIRCLE: ShapeKind = 2;
/// A curve around two focal points. /// A curve around two focal points.
const ELLIPSE: ShapeKind = 3; const ELLIPSE: ShapeKind = 3;
/// Whether a shape kind is curvy.
fn is_round(kind: ShapeKind) -> bool {
matches!(kind, CIRCLE | ELLIPSE)
}
/// Whether a shape kind has equal side length.
fn is_quadratic(kind: ShapeKind) -> bool {
matches!(kind, SQUARE | CIRCLE)
}

View File

@ -353,7 +353,8 @@ impl Layout for FillNode {
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?; let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames { for frame in &mut frames {
let shape = Geometry::Rect(frame.size).filled(self.fill); let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero()))
.filled(self.fill);
Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
} }
Ok(frames) Ok(frames)
@ -378,7 +379,8 @@ impl Layout for StrokeNode {
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?; let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames { for frame in &mut frames {
let shape = Geometry::Rect(frame.size).stroked(self.stroke); let shape = Geometry::Rect(frame.size, Sides::splat(Length::zero()))
.stroked(self.stroke);
Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
} }
Ok(frames) Ok(frames)

View File

@ -459,6 +459,22 @@ where
} }
} }
impl<T> Fold for Sides<Option<T>>
where
T: Default,
{
type Output = Sides<T>;
fn fold(self, outer: Self::Output) -> Self::Output {
Sides {
left: self.left.unwrap_or(outer.left),
right: self.right.unwrap_or(outer.right),
top: self.top.unwrap_or(outer.top),
bottom: self.bottom.unwrap_or(outer.bottom),
}
}
}
/// A scoped property barrier. /// A scoped property barrier.
/// ///
/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style /// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style