mirror of
https://github.com/typst/typst
synced 2025-06-28 00:03:17 +08:00
Add round corners and change arguments
This commit is contained in:
parent
f9e115daf5
commit
5f1499d380
@ -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) {
|
||||
|
@ -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(())
|
||||
|
177
src/frame.rs
177
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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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)
|
||||
};
|
||||
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)?;
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user