mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
206 lines
6.5 KiB
Rust
206 lines
6.5 KiB
Rust
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<Content>);
|
|
|
|
/// 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>;
|
|
|
|
#[node(Layout)]
|
|
impl<const S: ShapeKind> ShapeNode<S> {
|
|
/// How to fill the shape.
|
|
pub const FILL: Option<Paint> = None;
|
|
/// How to stroke the shape.
|
|
#[property(skip, resolve, fold)]
|
|
pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto;
|
|
|
|
/// How much to pad the shape's content.
|
|
#[property(resolve, fold)]
|
|
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
|
/// How much to extend the shape's dimensions beyond the allocated space.
|
|
#[property(resolve, fold)]
|
|
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
|
|
|
|
/// How much to round the shape's corners.
|
|
#[property(skip, resolve, fold)]
|
|
pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
|
|
|
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
|
let size = match S {
|
|
SQUARE => args.named::<Length>("size")?.map(Rel::from),
|
|
CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Rel::from(r)),
|
|
_ => None,
|
|
};
|
|
|
|
let width = match size {
|
|
None => args.named("width")?,
|
|
size => size,
|
|
};
|
|
|
|
let height = match size {
|
|
None => args.named("height")?,
|
|
size => size,
|
|
};
|
|
|
|
Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height)))
|
|
}
|
|
|
|
fn set(...) {
|
|
if is_round(S) {
|
|
styles.set_opt(
|
|
Self::STROKE,
|
|
args.named::<Smart<Option<RawStroke>>>("stroke")?
|
|
.map(|some| some.map(Sides::splat)),
|
|
);
|
|
} else {
|
|
styles.set_opt(Self::STROKE, args.named("stroke")?);
|
|
styles.set_opt(Self::RADIUS, args.named("radius")?);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<const S: ShapeKind> Layout for ShapeNode<S> {
|
|
fn layout(
|
|
&self,
|
|
world: Tracked<dyn World>,
|
|
regions: &Regions,
|
|
styles: StyleChain,
|
|
) -> SourceResult<Vec<Frame>> {
|
|
let mut frames;
|
|
if let Some(child) = &self.0 {
|
|
let mut inset = styles.get(Self::INSET);
|
|
if is_round(S) {
|
|
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
|
|
}
|
|
|
|
// Pad the child.
|
|
let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
|
|
|
|
let mut pod = Regions::one(regions.first, regions.base, regions.expand);
|
|
frames = child.layout_inline(world, &pod, styles)?;
|
|
|
|
for frame in frames.iter_mut() {
|
|
frame.apply_role(Role::GenericBlock);
|
|
}
|
|
|
|
// 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 = Axes::splat(true);
|
|
frames = child.layout_inline(world, &pod, styles)?;
|
|
}
|
|
} else {
|
|
// The default size that a shape takes on if it has no child and
|
|
// enough space.
|
|
let mut size = Size::new(Abs::pt(45.0), Abs::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![Frame::new(size)];
|
|
}
|
|
|
|
let frame = &mut frames[0];
|
|
|
|
// Add fill and/or stroke.
|
|
let fill = styles.get(Self::FILL);
|
|
let stroke = match styles.get(Self::STROKE) {
|
|
Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
|
|
Smart::Auto => Sides::splat(None),
|
|
Smart::Custom(strokes) => {
|
|
strokes.map(|s| s.map(RawStroke::unwrap_or_default))
|
|
}
|
|
};
|
|
|
|
let outset = styles.get(Self::OUTSET).relative_to(frame.size());
|
|
let size = frame.size() + outset.sum_by_axis();
|
|
|
|
let radius = styles
|
|
.get(Self::RADIUS)
|
|
.map(|side| side.relative_to(size.x.min(size.y) / 2.0));
|
|
|
|
let pos = Point::new(-outset.left, -outset.top);
|
|
|
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
|
if is_round(S) {
|
|
let shape = ellipse(size, fill, stroke.left);
|
|
frame.prepend(pos, Element::Shape(shape));
|
|
} else {
|
|
frame.prepend_multiple(
|
|
rounded_rect(size, radius, fill, stroke)
|
|
.into_iter()
|
|
.map(|x| (pos, Element::Shape(x))),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Apply link if it exists.
|
|
if let Some(url) = styles.get(TextNode::LINK) {
|
|
frame.link(url.clone());
|
|
}
|
|
|
|
Ok(frames)
|
|
}
|
|
|
|
fn level(&self) -> Level {
|
|
Level::Inline
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|