summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/export/pdf.rs45
-rw-r--r--src/export/render.rs38
-rw-r--r--src/frame.rs177
-rw-r--r--src/geom/angle.rs45
-rw-r--r--src/geom/sides.rs46
-rw-r--r--src/geom/transform.rs24
-rw-r--r--src/library/graphics/shape.rs174
-rw-r--r--src/model/layout.rs6
-rw-r--r--src/model/styles.rs16
9 files changed, 474 insertions, 97 deletions
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 067eb277..f5401dfb 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -16,9 +16,9 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset;
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::{
- 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::Context;
@@ -499,16 +499,16 @@ impl<'a> PageExporter<'a> {
}
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;
}
match shape.geometry {
- Geometry::Rect(size) => {
+ Geometry::Rect(size, radius) => {
let w = size.x.to_f32();
let h = size.y.to_f32();
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) => {
@@ -530,16 +530,37 @@ impl<'a> PageExporter<'a> {
self.set_fill(fill);
}
- if let Some(stroke) = shape.stroke {
- self.set_stroke(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);
+ use_stroke = true;
+ }
}
- match (shape.fill, shape.stroke) {
- (None, None) => unreachable!(),
- (Some(_), None) => self.content.fill_nonzero(),
- (None, Some(_)) => self.content.stroke(),
- (Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(),
+ match (shape.fill, use_stroke) {
+ (None, false) => self.content.end_path(),
+ (Some(_), false) => self.content.fill_nonzero(),
+ (None, true) => self.content.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) {
diff --git a/src/export/render.rs b/src/export/render.rs
index c3b92d31..9c674acb 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -8,8 +8,9 @@ use ttf_parser::{GlyphId, OutlineBuilder};
use usvg::FitTo;
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::library::prelude::{rect_path, rect_paths};
use crate::Context;
/// Export a frame into a rendered image.
@@ -298,12 +299,7 @@ fn render_shape(
shape: &Shape,
) -> Option<()> {
let path = match shape.geometry {
- Geometry::Rect(size) => {
- 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::Rect(size, radius) => convert_path(&rect_path(size, radius))?,
Geometry::Ellipse(size) => convert_path(&geom::Path::ellipse(size))?,
Geometry::Line(target) => {
let mut builder = sk::PathBuilder::new();
@@ -315,7 +311,7 @@ fn render_shape(
if let Some(fill) = shape.fill {
let mut paint: sk::Paint = fill.into();
- if matches!(shape.geometry, Geometry::Rect(_)) {
+ if matches!(shape.geometry, Geometry::Rect(_, _)) {
paint.anti_alias = false;
}
@@ -323,11 +319,27 @@ fn render_shape(
canvas.fill_path(&path, &paint, rule, ts, mask);
}
- if let Some(Stroke { paint, thickness }) = shape.stroke {
- let paint = paint.into();
- let mut stroke = sk::Stroke::default();
- stroke.width = thickness.to_f32();
- canvas.stroke_path(&path, &paint, &stroke, ts, mask);
+ if shape.stroke.is_uniform() || !matches!(shape.geometry, Geometry::Rect(_, _)) {
+ if let Some(Stroke { paint, thickness }) = shape.stroke.top {
+ let paint = paint.into();
+ let mut stroke = sk::Stroke::default();
+ stroke.width = thickness.to_f32();
+ 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(())
diff --git a/src/frame.rs b/src/frame.rs
index 5ee6e77e..f889601e 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -1,11 +1,13 @@
//! Finished layouts.
use std::fmt::{self, Debug, Formatter, Write};
+use std::mem;
use std::sync::Arc;
use crate::font::FaceId;
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::util::{EcoString, MaybeShared};
@@ -306,7 +308,7 @@ pub struct Shape {
/// The shape's background fill.
pub fill: Option<Paint>,
/// The shape's border stroke.
- pub stroke: Option<Stroke>,
+ pub stroke: Sides<Option<Stroke>>,
}
/// A shape's geometry.
@@ -314,8 +316,8 @@ pub struct Shape {
pub enum Geometry {
/// A line to a point (relative to its position).
Line(Point),
- /// A rectangle with its origin in the topleft corner.
- Rect(Size),
+ /// A rectangle with its origin in the topleft corner and a border radius.
+ Rect(Size, Sides<Length>),
/// A ellipse with its origin in the topleft corner.
Ellipse(Size),
/// A bezier path.
@@ -328,7 +330,7 @@ impl Geometry {
Shape {
geometry: self,
fill: Some(fill),
- stroke: None,
+ stroke: Sides::splat(None),
}
}
@@ -337,7 +339,170 @@ impl Geometry {
Shape {
geometry: self,
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
+}
diff --git a/src/geom/angle.rs b/src/geom/angle.rs
index 888442f7..65270ebd 100644
--- a/src/geom/angle.rs
+++ b/src/geom/angle.rs
@@ -64,6 +64,51 @@ impl Angle {
pub fn cos(self) -> f64 {
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 {
diff --git a/src/geom/sides.rs b/src/geom/sides.rs
index 3584a1ce..255c21ee 100644
--- a/src/geom/sides.rs
+++ b/src/geom/sides.rs
@@ -31,6 +31,32 @@ impl<T> Sides<T> {
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>
@@ -100,4 +126,24 @@ impl Side {
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,
+ }
+ }
}
diff --git a/src/geom/transform.rs b/src/geom/transform.rs
index 28a1af80..de2a9781 100644
--- a/src/geom/transform.rs
+++ b/src/geom/transform.rs
@@ -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.
pub const fn translate(tx: Length, ty: Length) -> Self {
Self { tx, ty, ..Self::identity() }
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index 49c74c2f..7a1bfb1f 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -5,36 +5,46 @@ use crate::library::text::TextNode;
/// Place a node into a sizable and fillable shape.
#[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.
-pub type SquareNode = ShapeNode<SQUARE>;
+pub type SquareNode = AngularNode<SQUARE>;
/// Place a node into a rectangle.
-pub type RectNode = ShapeNode<RECT>;
+pub type RectNode = AngularNode<RECT>;
-/// Place a node into a circle.
-pub type CircleNode = ShapeNode<CIRCLE>;
+// /// Place a node into a sizable and fillable shape.
+// #[derive(Debug, Hash)]
+// pub struct RoundNode<const S: ShapeKind>(pub Option<LayoutNode>);
-/// Place a node into an ellipse.
-pub type EllipseNode = ShapeNode<ELLIPSE>;
+// /// Place a node into a circle.
+// pub type CircleNode = RoundNode<CIRCLE>;
+
+// /// Place a node into an ellipse.
+// pub type EllipseNode = RoundNode<ELLIPSE>;
#[node]
-impl<const S: ShapeKind> ShapeNode<S> {
+impl<const S: ShapeKind> AngularNode<S> {
/// How to fill the shape.
pub const FILL: Option<Paint> = None;
/// How to stroke the shape.
#[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.
- 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> {
- let size = match S {
- SQUARE => args.named::<RawLength>("size")?.map(Relative::from),
- CIRCLE => args.named::<RawLength>("radius")?.map(|r| 2.0 * Relative::from(r)),
- _ => None,
- };
+ let size = args.named::<RawLength>("size")?.map(Relative::from);
let width = match size {
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(
&self,
ctx: &mut Context,
@@ -61,50 +114,43 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames;
if let Some(child) = &self.0 {
- let mut padding = styles.get(Self::PADDING);
- if is_round(S) {
- padding.rel += Ratio::new(0.5 - SQRT_2 / 4.0);
- }
+ let inset = styles.get(Self::INSET);
// 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);
frames = child.layout(ctx, &pod, styles)?;
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
- if is_quadratic(S) {
- let length = if regions.expand.x || regions.expand.y {
- let target = regions.expand.select(regions.first, Size::zero());
- target.x.max(target.y)
- } else {
- let size = frames[0].size;
- let desired = size.x.max(size.y);
- desired.min(regions.first.x).min(regions.first.y)
- };
-
- pod.first = Size::splat(length);
- pod.expand = Spec::splat(true);
- frames = child.layout(ctx, &pod, styles)?;
- }
+ let length = if regions.expand.x || regions.expand.y {
+ let target = regions.expand.select(regions.first, Size::zero());
+ target.x.max(target.y)
+ } else {
+ let size = frames[0].size;
+ let desired = size.x.max(size.y);
+ desired.min(regions.first.x).min(regions.first.y)
+ };
+
+ pod.first = Size::splat(length);
+ pod.expand = Spec::splat(true);
+ frames = child.layout(ctx, &pod, styles)?;
} else {
// The default size that a shape takes on if it has no child and
// enough space.
let mut size =
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 target = regions.expand.select(regions.first, Size::zero());
- target.x.max(target.y)
- } else {
- size.x.min(size.y)
- };
- size = Size::splat(length);
+ let length = if regions.expand.x || regions.expand.y {
+ let target = regions.expand.select(regions.first, Size::zero());
+ target.x.max(target.y)
} else {
- size = regions.expand.select(regions.first, size);
- }
+ size.x.min(size.y)
+ };
+ size = Size::splat(length);
frames = vec![Arc::new(Frame::new(size))];
}
@@ -114,18 +160,28 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
// Add fill and/or stroke.
let fill = styles.get(Self::FILL);
let stroke = match styles.get(Self::STROKE) {
- Smart::Auto => fill.is_none().then(Stroke::default),
- Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default),
+ Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::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 geometry = if is_round(S) {
- Geometry::Ellipse(frame.size)
- } else {
- Geometry::Rect(frame.size)
- };
+ let radius = {
+ let radius = styles.get(Self::RADIUS);
- let shape = Shape { geometry, fill, stroke };
+ Sides {
+ 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),
+ }
+ };
+
+ 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));
}
@@ -152,13 +208,3 @@ const CIRCLE: ShapeKind = 2;
/// A curve around two focal points.
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)
-}
diff --git a/src/model/layout.rs b/src/model/layout.rs
index 51154286..63e8f088 100644
--- a/src/model/layout.rs
+++ b/src/model/layout.rs
@@ -353,7 +353,8 @@ impl Layout for FillNode {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?;
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));
}
Ok(frames)
@@ -378,7 +379,8 @@ impl Layout for StrokeNode {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?;
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));
}
Ok(frames)
diff --git a/src/model/styles.rs b/src/model/styles.rs
index eb7a7053..2e752625 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -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.
///
/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style