mirror of
https://github.com/typst/typst
synced 2025-05-15 09:35:28 +08:00
211 lines
6.7 KiB
Rust
211 lines
6.7 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 AngularNode<const S: ShapeKind>(pub Option<LayoutNode>);
|
|
|
|
/// Place a node into a square.
|
|
pub type SquareNode = AngularNode<SQUARE>;
|
|
|
|
/// Place a node into a rectangle.
|
|
pub type RectNode = AngularNode<RECT>;
|
|
|
|
// /// 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 a circle.
|
|
// pub type CircleNode = RoundNode<CIRCLE>;
|
|
|
|
// /// Place a node into an ellipse.
|
|
// pub type EllipseNode = RoundNode<ELLIPSE>;
|
|
|
|
#[node]
|
|
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<Sides<Option<RawStroke>>> = Smart::Auto;
|
|
|
|
/// How much to pad the shape's content.
|
|
#[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 = args.named::<RawLength>("size")?.map(Relative::from);
|
|
|
|
let width = match size {
|
|
None => args.named("width")?,
|
|
size => size,
|
|
};
|
|
|
|
let height = match size {
|
|
None => args.named("height")?,
|
|
size => size,
|
|
};
|
|
|
|
Ok(Content::inline(
|
|
Self(args.find()?).pack().sized(Spec::new(width, height)),
|
|
))
|
|
}
|
|
}
|
|
|
|
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,
|
|
regions: &Regions,
|
|
styles: StyleChain,
|
|
) -> TypResult<Vec<Arc<Frame>>> {
|
|
let mut frames;
|
|
if let Some(child) = &self.0 {
|
|
let inset = styles.get(Self::INSET);
|
|
|
|
// Pad the child.
|
|
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.
|
|
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);
|
|
|
|
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);
|
|
|
|
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 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| Some(s.unwrap_or_default())),
|
|
};
|
|
|
|
let radius = {
|
|
let radius = styles.get(Self::RADIUS);
|
|
|
|
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));
|
|
}
|
|
|
|
// Apply link if it exists.
|
|
if let Some(url) = styles.get(TextNode::LINK) {
|
|
frame.link(url.clone());
|
|
}
|
|
|
|
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;
|