diff options
Diffstat (limited to 'src/library/elements/shape.rs')
| -rw-r--r-- | src/library/elements/shape.rs | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/src/library/elements/shape.rs b/src/library/elements/shape.rs new file mode 100644 index 00000000..8b967412 --- /dev/null +++ b/src/library/elements/shape.rs @@ -0,0 +1,166 @@ +use std::f64::consts::SQRT_2; + +use crate::library::prelude::*; +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>); + +/// Place a node into a square. +pub type SquareNode = ShapeNode<SQUARE>; + +/// Place a node into a rectangle. +pub type RectNode = ShapeNode<RECT>; + +/// Place a node into a circle. +pub type CircleNode = ShapeNode<CIRCLE>; + +/// Place a node into an ellipse. +pub type EllipseNode = ShapeNode<ELLIPSE>; + +#[class] +impl<const S: ShapeKind> ShapeNode<S> { + /// How to fill the shape. + pub const FILL: Option<Paint> = None; + /// How the stroke the shape. + pub const STROKE: Smart<Option<Paint>> = Smart::Auto; + /// The stroke's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + /// How much to pad the shape's content. + pub const PADDING: Linear = Linear::zero(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let size = match S { + SQUARE => args.named::<Length>("size")?.map(Linear::from), + CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Linear::from(r)), + _ => None, + }; + + let width = match size { + None => args.named("width")?, + size => size, + }; + + let height = match size { + None => args.named("height")?, + size => size, + }; + + Ok(Template::inline( + Self(args.find()?).pack().sized(Spec::new(width, height)), + )) + } +} + +impl<const S: ShapeKind> Layout for ShapeNode<S> { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> 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 += Relative::new(0.5 - SQRT_2 / 4.0); + } + + // Pad the child. + let child = child.clone().padded(Sides::splat(padding)); + + 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)?; + } + } 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); + } else { + size = regions.expand.select(regions.first, size); + } + + frames = vec![Arc::new(Frame::new(size))]; + } + + let frame = Arc::make_mut(&mut frames[0]); + + // Add fill and/or stroke. + let fill = styles.get(Self::FILL); + let thickness = styles.get(Self::THICKNESS); + let stroke = styles + .get(Self::STROKE) + .unwrap_or(fill.is_none().then(|| Color::BLACK.into())) + .map(|paint| Stroke { paint, thickness }); + + if fill.is_some() || stroke.is_some() { + let geometry = if is_round(S) { + Geometry::Ellipse(frame.size) + } else { + Geometry::Rect(frame.size) + }; + + let shape = Shape { geometry, fill, stroke }; + frame.prepend(Point::zero(), Element::Shape(shape)); + } + + // Apply link if it exists. + if let Some(url) = styles.get_ref(TextNode::LINK) { + frame.link(url); + } + + Ok(frames) + } +} + +/// A category of shape. +pub type ShapeKind = usize; + +/// A rectangle with equal side lengths. +const SQUARE: ShapeKind = 0; + +/// A quadrilateral with four right angles. +const RECT: ShapeKind = 1; + +/// An ellipse with coinciding foci. +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) +} |
