mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
commit
aa10ea8470
@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
use super::{ops, Args, Array, Dict, Func, RawLength};
|
||||
use crate::diag::{with_alternative, StrResult};
|
||||
use crate::geom::{
|
||||
Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor,
|
||||
Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides,
|
||||
};
|
||||
use crate::library::text::RawNode;
|
||||
use crate::model::{Content, Layout, LayoutNode};
|
||||
@ -596,6 +596,47 @@ impl<T: Cast> Cast for Smart<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Cast for Sides<T>
|
||||
where
|
||||
T: Cast + Default + Clone,
|
||||
{
|
||||
fn is(value: &Value) -> bool {
|
||||
matches!(value, Value::Dict(_)) || T::is(value)
|
||||
}
|
||||
|
||||
fn cast(value: Value) -> StrResult<Self> {
|
||||
match value {
|
||||
Value::Dict(dict) => {
|
||||
for (key, _) in &dict {
|
||||
if !matches!(
|
||||
key.as_str(),
|
||||
"left" | "top" | "right" | "bottom" | "x" | "y" | "rest"
|
||||
) {
|
||||
return Err(format!("unexpected key {key:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
let sides = Sides {
|
||||
left: dict.get("left".into()).or_else(|_| dict.get("x".into())),
|
||||
top: dict.get("top".into()).or_else(|_| dict.get("y".into())),
|
||||
right: dict.get("right".into()).or_else(|_| dict.get("x".into())),
|
||||
bottom: dict.get("bottom".into()).or_else(|_| dict.get("y".into())),
|
||||
}
|
||||
.map(|side| {
|
||||
side.or_else(|_| dict.get("rest".into()))
|
||||
.and_then(|v| T::cast(v.clone()))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
Ok(sides)
|
||||
}
|
||||
v => T::cast(v)
|
||||
.map(Sides::splat)
|
||||
.map_err(|msg| with_alternative(msg, "dictionary")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
Dir: "direction",
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ 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::{Element, Frame, Group, Text};
|
||||
use crate::geom::{
|
||||
self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform,
|
||||
self, Color, Em, Geometry, Length, Numeric, Paint, Point, Shape, Size, Stroke,
|
||||
Transform,
|
||||
};
|
||||
use crate::image::{Image, ImageId, ImageStore, RasterImage};
|
||||
use crate::Context;
|
||||
|
@ -7,8 +7,10 @@ use tiny_skia as sk;
|
||||
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::frame::{Element, Frame, Group, Text};
|
||||
use crate::geom::{
|
||||
self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform,
|
||||
};
|
||||
use crate::image::{Image, RasterImage, Svg};
|
||||
use crate::Context;
|
||||
|
||||
|
54
src/frame.rs
54
src/frame.rs
@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::font::FaceId;
|
||||
use crate::geom::{
|
||||
Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform,
|
||||
Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform,
|
||||
};
|
||||
use crate::image::ImageId;
|
||||
use crate::util::{EcoString, MaybeShared};
|
||||
@ -40,6 +40,14 @@ impl Frame {
|
||||
self.elements.insert(0, (pos, element));
|
||||
}
|
||||
|
||||
/// Add multiple elements at a position in the background.
|
||||
pub fn prepend_multiple<I>(&mut self, insert: I)
|
||||
where
|
||||
I: IntoIterator<Item = (Point, Element)>,
|
||||
{
|
||||
self.elements.splice(0 .. 0, insert);
|
||||
}
|
||||
|
||||
/// Add an element at a position in the foreground.
|
||||
pub fn push(&mut self, pos: Point, element: Element) {
|
||||
self.elements.push((pos, element));
|
||||
@ -297,47 +305,3 @@ pub struct Glyph {
|
||||
/// The first character of the glyph's cluster.
|
||||
pub c: char,
|
||||
}
|
||||
|
||||
/// A geometric shape with optional fill and stroke.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Shape {
|
||||
/// The shape's geometry.
|
||||
pub geometry: Geometry,
|
||||
/// The shape's background fill.
|
||||
pub fill: Option<Paint>,
|
||||
/// The shape's border stroke.
|
||||
pub stroke: Option<Stroke>,
|
||||
}
|
||||
|
||||
/// A shape's geometry.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
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 ellipse with its origin in the topleft corner.
|
||||
Ellipse(Size),
|
||||
/// A bezier path.
|
||||
Path(Path),
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
/// Fill the geometry without a stroke.
|
||||
pub fn filled(self, fill: Paint) -> Shape {
|
||||
Shape {
|
||||
geometry: self,
|
||||
fill: Some(fill),
|
||||
stroke: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke the geometry without a fill.
|
||||
pub fn stroked(self, stroke: Stroke) -> Shape {
|
||||
Shape {
|
||||
geometry: self,
|
||||
fill: None,
|
||||
stroke: Some(stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ mod paint;
|
||||
mod path;
|
||||
mod point;
|
||||
mod ratio;
|
||||
mod rect;
|
||||
mod relative;
|
||||
mod scalar;
|
||||
mod sides;
|
||||
@ -30,6 +31,7 @@ pub use paint::*;
|
||||
pub use path::*;
|
||||
pub use point::*;
|
||||
pub use ratio::*;
|
||||
pub use rect::*;
|
||||
pub use relative::*;
|
||||
pub use scalar::*;
|
||||
pub use sides::*;
|
||||
@ -60,6 +62,50 @@ pub trait Get<Index> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A geometric shape with optional fill and stroke.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Shape {
|
||||
/// The shape's geometry.
|
||||
pub geometry: Geometry,
|
||||
/// The shape's background fill.
|
||||
pub fill: Option<Paint>,
|
||||
/// The shape's border stroke.
|
||||
pub stroke: Option<Stroke>,
|
||||
}
|
||||
|
||||
/// A shape's geometry.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
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 ellipse with its origin in the topleft corner.
|
||||
Ellipse(Size),
|
||||
/// A bezier path.
|
||||
Path(Path),
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
/// Fill the geometry without a stroke.
|
||||
pub fn filled(self, fill: Paint) -> Shape {
|
||||
Shape {
|
||||
geometry: self,
|
||||
fill: Some(fill),
|
||||
stroke: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke the geometry without a fill.
|
||||
pub fn stroked(self, stroke: Stroke) -> Shape {
|
||||
Shape {
|
||||
geometry: self,
|
||||
fill: None,
|
||||
stroke: Some(stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A numeric type.
|
||||
pub trait Numeric:
|
||||
Sized
|
||||
|
@ -71,3 +71,22 @@ impl Path {
|
||||
self.0.push(PathElement::ClosePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the control points for a bezier curve that describes a circular arc for
|
||||
/// a start point, an end point and a center of the circle whose arc connects
|
||||
/// the two.
|
||||
pub fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] {
|
||||
// https://stackoverflow.com/a/44829356/1567835
|
||||
let a = start - center;
|
||||
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);
|
||||
|
||||
[start, control_1, control_2, end]
|
||||
}
|
||||
|
184
src/geom/rect.rs
Normal file
184
src/geom/rect.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use super::*;
|
||||
|
||||
use std::mem;
|
||||
|
||||
/// A rectangle with rounded corners.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Rect {
|
||||
size: Size,
|
||||
radius: Sides<Length>,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Create a new rectangle.
|
||||
pub fn new(size: Size, radius: Sides<Length>) -> Self {
|
||||
Self { size, radius }
|
||||
}
|
||||
|
||||
/// Output all constituent shapes of the rectangle in order. The last one is
|
||||
/// in the foreground. The function will output multiple items if the stroke
|
||||
/// properties differ by side.
|
||||
pub fn shapes(
|
||||
self,
|
||||
fill: Option<Paint>,
|
||||
stroke: Sides<Option<Stroke>>,
|
||||
) -> Vec<Shape> {
|
||||
let mut res = vec![];
|
||||
if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) {
|
||||
res.push(Shape {
|
||||
geometry: self.fill_geometry(),
|
||||
fill,
|
||||
stroke: if stroke.is_uniform() { stroke.top } else { None },
|
||||
});
|
||||
}
|
||||
|
||||
if !stroke.is_uniform() {
|
||||
for (path, stroke) in self.stroke_segments(stroke) {
|
||||
if stroke.is_some() {
|
||||
res.push(Shape {
|
||||
geometry: Geometry::Path(path),
|
||||
fill: None,
|
||||
stroke,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Output the shape of the rectangle as a path or primitive rectangle,
|
||||
/// depending on whether it is rounded.
|
||||
fn fill_geometry(self) -> Geometry {
|
||||
if self.radius.iter().copied().all(Length::is_zero) {
|
||||
Geometry::Rect(self.size)
|
||||
} else {
|
||||
let mut paths = self.stroke_segments(Sides::splat(None));
|
||||
assert_eq!(paths.len(), 1);
|
||||
|
||||
Geometry::Path(paths.pop().unwrap().0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Output the minimum number of paths along the rectangles border.
|
||||
fn stroke_segments(
|
||||
self,
|
||||
strokes: Sides<Option<Stroke>>,
|
||||
) -> Vec<(Path, Option<Stroke>)> {
|
||||
let mut res = vec![];
|
||||
|
||||
let mut connection = Connection::default();
|
||||
let mut path = Path::new();
|
||||
let mut always_continuous = true;
|
||||
|
||||
for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] {
|
||||
let is_continuous = strokes.get(side) == strokes.get(side.next_cw());
|
||||
connection = connection.advance(is_continuous && side != Side::Left);
|
||||
always_continuous &= is_continuous;
|
||||
|
||||
draw_side(
|
||||
&mut path,
|
||||
side,
|
||||
self.size,
|
||||
self.radius.get(side.next_ccw()),
|
||||
self.radius.get(side),
|
||||
connection,
|
||||
);
|
||||
|
||||
if !is_continuous {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 });
|
||||
let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 });
|
||||
|
||||
let length = size.get(side.axis());
|
||||
|
||||
// The arcs for a border of the rectangle along the x-axis, starting at (0,0).
|
||||
let p1 = Point::with_x(radius_left);
|
||||
let mut arc1 = bezier_arc(
|
||||
p1 + Point::new(
|
||||
-angle_left.sin() * radius_left,
|
||||
(1.0 - angle_left.cos()) * radius_left,
|
||||
),
|
||||
Point::new(radius_left, radius_left),
|
||||
p1,
|
||||
);
|
||||
|
||||
let p2 = Point::with_x(length - radius_right);
|
||||
let mut arc2 = bezier_arc(
|
||||
p2,
|
||||
Point::new(length - radius_right, radius_right),
|
||||
p2 + Point::new(
|
||||
angle_right.sin() * radius_right,
|
||||
(1.0 - angle_right.cos()) * radius_right,
|
||||
),
|
||||
);
|
||||
|
||||
let transform = match side {
|
||||
Side::Left => Transform::rotate(Angle::deg(-90.0))
|
||||
.post_concat(Transform::translate(Length::zero(), size.y)),
|
||||
Side::Bottom => Transform::rotate(Angle::deg(180.0))
|
||||
.post_concat(Transform::translate(size.x, size.y)),
|
||||
Side::Right => Transform::rotate(Angle::deg(90.0))
|
||||
.post_concat(Transform::translate(size.x, Length::zero())),
|
||||
_ => Transform::identity(),
|
||||
};
|
||||
|
||||
arc1 = arc1.map(|x| x.transform(transform));
|
||||
arc2 = arc2.map(|x| x.transform(transform));
|
||||
|
||||
if !connection.prev {
|
||||
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.next && !radius_right.is_zero() {
|
||||
path.cubic_to(arc2[1], arc2[2], arc2[3]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A state machine that indicates which sides of the border strokes in a 2D
|
||||
/// polygon are connected to their neighboring sides.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
|
||||
struct Connection {
|
||||
prev: bool,
|
||||
next: bool,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Advance to the next clockwise side of the polygon. The argument
|
||||
/// indicates whether the border is connected on the right side of the next
|
||||
/// edge.
|
||||
pub fn advance(self, next: bool) -> Self {
|
||||
Self { prev: self.next, next }
|
||||
}
|
||||
}
|
@ -31,6 +31,45 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Zip two instances into an instance.
|
||||
pub fn zip<F, V, W>(self, other: Sides<V>, mut f: F) -> Sides<W>
|
||||
where
|
||||
F: FnMut(T, V, Side) -> W,
|
||||
{
|
||||
Sides {
|
||||
left: f(self.left, other.left, Side::Left),
|
||||
top: f(self.top, other.top, Side::Top),
|
||||
right: f(self.right, other.right, Side::Right),
|
||||
bottom: f(self.bottom, other.bottom, Side::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 +139,32 @@ impl Side {
|
||||
Self::Bottom => Self::Top,
|
||||
}
|
||||
}
|
||||
|
||||
/// The next side, clockwise.
|
||||
pub fn next_cw(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 next_ccw(self) -> Self {
|
||||
match self {
|
||||
Self::Left => Self::Bottom,
|
||||
Self::Top => Self::Left,
|
||||
Self::Right => Self::Top,
|
||||
Self::Bottom => Self::Right,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the corresponding axis.
|
||||
pub fn axis(self) -> SpecAxis {
|
||||
match self {
|
||||
Self::Left | Self::Right => SpecAxis::Vertical,
|
||||
Self::Top | Self::Bottom => SpecAxis::Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ impl Transform {
|
||||
}
|
||||
|
||||
/// Pre-concatenate another transformation.
|
||||
pub fn pre_concat(&self, prev: Self) -> Self {
|
||||
pub fn pre_concat(self, prev: Self) -> Self {
|
||||
Transform {
|
||||
sx: self.sx * prev.sx + self.kx * prev.ky,
|
||||
ky: self.ky * prev.sx + self.sy * prev.ky,
|
||||
@ -63,6 +63,11 @@ impl Transform {
|
||||
ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Post-concatenate another transformation.
|
||||
pub fn post_concat(self, next: Self) -> Self {
|
||||
next.pre_concat(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Transform {
|
||||
|
@ -25,9 +25,19 @@ impl<const S: ShapeKind> ShapeNode<S> {
|
||||
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 {
|
||||
@ -50,6 +60,30 @@ impl<const S: ShapeKind> ShapeNode<S> {
|
||||
Self(args.find()?).pack().sized(Spec::new(width, height)),
|
||||
))
|
||||
}
|
||||
|
||||
fn set(args: &mut Args) -> TypResult<StyleMap> {
|
||||
let mut styles = StyleMap::new();
|
||||
styles.set_opt(Self::FILL, args.named("fill")?);
|
||||
|
||||
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::INSET, args.named("inset")?);
|
||||
styles.set_opt(Self::OUTSET, args.named("outset")?);
|
||||
|
||||
if !is_round(S) {
|
||||
styles.set_opt(Self::RADIUS, args.named("radius")?);
|
||||
}
|
||||
|
||||
Ok(styles)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const S: ShapeKind> Layout for ShapeNode<S> {
|
||||
@ -61,13 +95,13 @@ 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);
|
||||
let mut inset = styles.get(Self::INSET);
|
||||
if is_round(S) {
|
||||
padding.rel += Ratio::new(0.5 - SQRT_2 / 4.0);
|
||||
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
|
||||
}
|
||||
|
||||
// Pad the child.
|
||||
let child = child.clone().padded(Sides::splat(padding));
|
||||
let child = child.clone().padded(inset.map(|side| side.map(RawLength::from)));
|
||||
|
||||
let mut pod = Regions::one(regions.first, regions.base, regions.expand);
|
||||
frames = child.layout(ctx, &pod, styles)?;
|
||||
@ -114,19 +148,38 @@ 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| s.map(RawStroke::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 outset = styles.get(Self::OUTSET).relative_to(frame.size);
|
||||
let size = frame.size + outset.sum_by_axis();
|
||||
|
||||
let shape = Shape { geometry, fill, stroke };
|
||||
frame.prepend(Point::zero(), Element::Shape(shape));
|
||||
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 = Shape {
|
||||
geometry: Geometry::Ellipse(size),
|
||||
fill,
|
||||
stroke: stroke.left,
|
||||
};
|
||||
frame.prepend(pos, Element::Shape(shape));
|
||||
} else {
|
||||
frame.prepend_multiple(
|
||||
Rect::new(size, radius)
|
||||
.shapes(fill, stroke)
|
||||
.into_iter()
|
||||
.map(|x| (pos, Element::Shape(x))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply link if it exists.
|
||||
|
@ -18,14 +18,10 @@ impl PageNode {
|
||||
/// Whether the page is flipped into landscape orientation.
|
||||
pub const FLIPPED: bool = false;
|
||||
|
||||
/// The left margin.
|
||||
pub const LEFT: Smart<Relative<RawLength>> = Smart::Auto;
|
||||
/// The right margin.
|
||||
pub const RIGHT: Smart<Relative<RawLength>> = Smart::Auto;
|
||||
/// The top margin.
|
||||
pub const TOP: Smart<Relative<RawLength>> = Smart::Auto;
|
||||
/// The bottom margin.
|
||||
pub const BOTTOM: Smart<Relative<RawLength>> = Smart::Auto;
|
||||
/// The page margin.
|
||||
#[property(fold)]
|
||||
pub const MARGINS: Sides<Option<Smart<Relative<RawLength>>>> =
|
||||
Sides::splat(Smart::Auto);
|
||||
|
||||
/// How many columns the page has.
|
||||
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
|
||||
@ -53,15 +49,7 @@ impl PageNode {
|
||||
|
||||
styles.set_opt(Self::WIDTH, args.named("width")?);
|
||||
styles.set_opt(Self::HEIGHT, args.named("height")?);
|
||||
|
||||
let all = args.named("margins")?;
|
||||
let hor = args.named("horizontal")?;
|
||||
let ver = args.named("vertical")?;
|
||||
styles.set_opt(Self::LEFT, args.named("left")?.or(hor).or(all));
|
||||
styles.set_opt(Self::TOP, args.named("top")?.or(ver).or(all));
|
||||
styles.set_opt(Self::RIGHT, args.named("right")?.or(hor).or(all));
|
||||
styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(ver).or(all));
|
||||
|
||||
styles.set_opt(Self::MARGINS, args.named("margins")?);
|
||||
styles.set_opt(Self::FLIPPED, args.named("flipped")?);
|
||||
styles.set_opt(Self::FILL, args.named("fill")?);
|
||||
styles.set_opt(Self::COLUMNS, args.named("columns")?);
|
||||
@ -96,12 +84,7 @@ impl PageNode {
|
||||
|
||||
// Determine the margins.
|
||||
let default = Relative::from(0.1190 * min);
|
||||
let padding = Sides {
|
||||
left: styles.get(Self::LEFT).unwrap_or(default),
|
||||
right: styles.get(Self::RIGHT).unwrap_or(default),
|
||||
top: styles.get(Self::TOP).unwrap_or(default),
|
||||
bottom: styles.get(Self::BOTTOM).unwrap_or(default),
|
||||
};
|
||||
let padding = styles.get(Self::MARGINS).map(|side| side.unwrap_or(default));
|
||||
|
||||
let mut child = self.0.clone();
|
||||
|
||||
|
@ -8,8 +8,10 @@ use std::sync::Arc;
|
||||
use super::{Barrier, NodeId, Resolve, StyleChain, StyleEntry};
|
||||
use crate::diag::TypResult;
|
||||
use crate::eval::{RawAlign, RawLength};
|
||||
use crate::frame::{Element, Frame, Geometry};
|
||||
use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke};
|
||||
use crate::frame::{Element, Frame};
|
||||
use crate::geom::{
|
||||
Align, Geometry, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke,
|
||||
};
|
||||
use crate::library::graphics::MoveNode;
|
||||
use crate::library::layout::{AlignNode, PadNode};
|
||||
use crate::util::Prehashed;
|
||||
|
@ -7,8 +7,8 @@ use std::sync::Arc;
|
||||
|
||||
use super::{Content, Show, ShowNode};
|
||||
use crate::diag::{At, TypResult};
|
||||
use crate::eval::{Args, Func, Node, Smart, Value};
|
||||
use crate::geom::{Numeric, Relative, Sides, Spec};
|
||||
use crate::eval::{Args, Func, Node, RawLength, Smart, Value};
|
||||
use crate::geom::{Length, Numeric, Relative, Sides, Spec};
|
||||
use crate::library::layout::PageNode;
|
||||
use crate::library::structure::{EnumNode, ListNode};
|
||||
use crate::library::text::{FontFamily, ParNode, TextNode};
|
||||
@ -459,6 +459,33 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Fold for Sides<T>
|
||||
where
|
||||
T: Fold,
|
||||
{
|
||||
type Output = Sides<T::Output>;
|
||||
|
||||
fn fold(self, outer: Self::Output) -> Self::Output {
|
||||
self.zip(outer, |inner, outer, _| inner.fold(outer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Fold for Sides<Option<Relative<Length>>> {
|
||||
type Output = Sides<Relative<Length>>;
|
||||
|
||||
fn fold(self, outer: Self::Output) -> Self::Output {
|
||||
self.zip(outer, |inner, outer, _| inner.unwrap_or(outer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Fold for Sides<Option<Smart<Relative<RawLength>>>> {
|
||||
type Output = Sides<Smart<Relative<RawLength>>>;
|
||||
|
||||
fn fold(self, outer: Self::Output) -> Self::Output {
|
||||
self.zip(outer, |inner, outer, _| inner.unwrap_or(outer))
|
||||
}
|
||||
}
|
||||
|
||||
/// A scoped property barrier.
|
||||
///
|
||||
/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 14 KiB |
@ -11,7 +11,7 @@
|
||||
|
||||
// Syntax sugar for function definitions.
|
||||
#let fill = conifer
|
||||
#let rect(body) = rect(width: 2cm, fill: fill, padding: 5pt, body)
|
||||
#let rect(body) = rect(width: 2cm, fill: fill, inset: 5pt, body)
|
||||
#rect[Hi!]
|
||||
|
||||
---
|
||||
|
@ -7,6 +7,6 @@
|
||||
#let d = 3
|
||||
#let value = [hi]
|
||||
#let item(a, b) = a + b
|
||||
#let fn = rect.with(fill: conifer, padding: 5pt)
|
||||
#let fn = rect.with(fill: conifer, inset: 5pt)
|
||||
|
||||
Some _includable_ text.
|
||||
|
@ -11,7 +11,7 @@
|
||||
---
|
||||
// Test alignment in automatically sized square and circle.
|
||||
#set text(8pt)
|
||||
#square(padding: 4pt)[
|
||||
#square(inset: 4pt)[
|
||||
Hey there, #align(center + bottom, rotate(180deg, [you!]))
|
||||
]
|
||||
#circle(align(center + horizon, [Hey.]))
|
||||
|
@ -16,13 +16,13 @@ Auto-sized circle. \
|
||||
Center-aligned rect in auto-sized circle.
|
||||
#circle(fill: forest, stroke: conifer,
|
||||
align(center + horizon,
|
||||
rect(fill: conifer, padding: 5pt)[But, soft!]
|
||||
rect(fill: conifer, inset: 5pt)[But, soft!]
|
||||
)
|
||||
)
|
||||
|
||||
Rect in auto-sized circle. \
|
||||
#circle(fill: forest,
|
||||
rect(fill: conifer, stroke: white, padding: 4pt)[
|
||||
rect(fill: conifer, stroke: white, inset: 4pt)[
|
||||
#set text(8pt)
|
||||
But, soft! what light through yonder window breaks?
|
||||
]
|
||||
|
@ -17,7 +17,10 @@ Rect in ellipse in fixed rect. \
|
||||
)
|
||||
|
||||
Auto-sized ellipse. \
|
||||
#ellipse(fill: conifer, stroke: 3pt + forest, padding: 3pt)[
|
||||
#ellipse(fill: conifer, stroke: 3pt + forest, inset: 3pt)[
|
||||
#set text(8pt)
|
||||
But, soft! what light through yonder window breaks?
|
||||
]
|
||||
|
||||
|
||||
An inline #ellipse(width: 8pt, height: 6pt, outset: (top: 3pt, rest: 5.5pt)) ellipse.
|
@ -38,3 +38,13 @@
|
||||
#sq(stroke: blue)
|
||||
#sq(fill: teal, stroke: blue)
|
||||
#sq(fill: teal, stroke: 2pt + blue)
|
||||
|
||||
---
|
||||
// Test stroke composition.
|
||||
#set square(stroke: 4pt)
|
||||
#set text("Roboto")
|
||||
#square(
|
||||
stroke: (left: red, top: yellow, right: green, bottom: blue),
|
||||
radius: 100%, align(center+horizon)[*G*],
|
||||
inset: 8pt
|
||||
)
|
||||
|
@ -8,7 +8,7 @@
|
||||
#set page(width: 150pt)
|
||||
|
||||
// Fit to text.
|
||||
#rect(fill: conifer, padding: 3pt)[Textbox]
|
||||
#rect(fill: conifer, inset: 3pt)[Textbox]
|
||||
|
||||
// Empty with fixed width and height.
|
||||
#block(rect(
|
||||
@ -18,7 +18,7 @@
|
||||
))
|
||||
|
||||
// Fixed width, text height.
|
||||
#rect(width: 2cm, fill: rgb("9650d6"), padding: 5pt)[Fixed and padded]
|
||||
#rect(width: 2cm, fill: rgb("9650d6"), inset: 5pt)[Fixed and padded]
|
||||
|
||||
// Page width, fixed height.
|
||||
#rect(height: 1cm, width: 100%, fill: rgb("734ced"))[Topleft]
|
||||
@ -27,3 +27,30 @@
|
||||
\{#rect(width: 0.5in, height: 7pt, fill: rgb("d6cd67"))
|
||||
#rect(width: 0.5in, height: 7pt, fill: rgb("edd466"))
|
||||
#rect(width: 0.5in, height: 7pt, fill: rgb("e3be62"))\}
|
||||
|
||||
// Rounded corners.
|
||||
#rect(width: 2cm, radius: 60%)
|
||||
#rect(width: 1cm, radius: (x: 5pt, y: 10pt))
|
||||
#rect(width: 1.25cm, radius: (left: 2pt, top: 5pt, right: 8pt, bottom: 11pt))
|
||||
|
||||
// Different strokes.
|
||||
[
|
||||
#set rect(stroke: (right: red))
|
||||
#rect(width: 100%, fill: lime, stroke: (x: 5pt, y: 1pt))
|
||||
]
|
||||
|
||||
---
|
||||
// Outset padding.
|
||||
#show node: raw as [
|
||||
#set text("IBM Plex Mono", 8pt)
|
||||
#h(.7em, weak: true)
|
||||
#rect(radius: 3pt, outset: (y: 3pt, x: 2.5pt), fill: rgb(239, 241, 243))[{node.text}]
|
||||
#h(.7em, weak: true)
|
||||
]
|
||||
|
||||
Use the `*const ptr` pointer.
|
||||
|
||||
---
|
||||
// Error: 15-38 unexpected key "cake"
|
||||
#rect(radius: (left: 10pt, cake: 5pt))
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
---
|
||||
// Test auto-sized square.
|
||||
#square(fill: eastern, padding: 5pt)[
|
||||
#square(fill: eastern, inset: 5pt)[
|
||||
#set text(fill: white, weight: "bold")
|
||||
Typst
|
||||
]
|
||||
|
@ -16,7 +16,7 @@
|
||||
// Test the `columns` function.
|
||||
#set page(width: auto)
|
||||
|
||||
#rect(width: 180pt, height: 100pt, padding: 8pt, columns(2, [
|
||||
#rect(width: 180pt, height: 100pt, inset: 8pt, columns(2, [
|
||||
A special plight has befallen our document.
|
||||
Columns in text boxes reigned down unto the soil
|
||||
to waste a year's crop of rich layouts.
|
||||
@ -40,7 +40,7 @@ a page for a test but it does get the job done.
|
||||
// Test the expansion behavior.
|
||||
#set page(height: 2.5cm, width: 7.05cm)
|
||||
|
||||
#rect(padding: 6pt, columns(2, [
|
||||
#rect(inset: 6pt, columns(2, [
|
||||
ABC \
|
||||
BCD
|
||||
#colbreak()
|
||||
@ -73,7 +73,7 @@ D
|
||||
// Test an empty second column.
|
||||
#set page(width: 7.05cm, columns: 2)
|
||||
|
||||
#rect(width: 100%, padding: 3pt)[So there isn't anything in the second column?]
|
||||
#rect(width: 100%, inset: 3pt)[So there isn't anything in the second column?]
|
||||
|
||||
---
|
||||
// Test columns when one of them is empty.
|
||||
|
@ -11,10 +11,10 @@
|
||||
---
|
||||
// Set individual margins.
|
||||
#set page(height: 40pt)
|
||||
[#set page(left: 0pt); #align(left)[Left]]
|
||||
[#set page(right: 0pt); #align(right)[Right]]
|
||||
[#set page(top: 0pt); #align(top)[Top]]
|
||||
[#set page(bottom: 0pt); #align(bottom)[Bottom]]
|
||||
[#set page(margins: (left: 0pt)); #align(left)[Left]]
|
||||
[#set page(margins: (right: 0pt)); #align(right)[Right]]
|
||||
[#set page(margins: (top: 0pt)); #align(top)[Top]]
|
||||
[#set page(margins: (bottom: 0pt)); #align(bottom)[Bottom]]
|
||||
|
||||
// Ensure that specific margins override general margins.
|
||||
[#set page(margins: 0pt, left: 20pt); Overriden]
|
||||
[#set page(margins: (rest: 0pt, left: 20pt)); Overriden]
|
||||
|
@ -1,7 +1,6 @@
|
||||
#set page(
|
||||
paper: "a8",
|
||||
margins: 30pt,
|
||||
horizontal: 15pt,
|
||||
margins: (x: 15pt, y: 30pt),
|
||||
header: align(horizon, {
|
||||
text(eastern)[*Typst*]
|
||||
h(1fr)
|
||||
@ -18,5 +17,5 @@ do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she
|
||||
were! She speaks yet she says nothing: what of that? Her eye discourses; I will
|
||||
answer it.
|
||||
|
||||
#set page(header: none, height: auto, top: 15pt, bottom: 25pt)
|
||||
#set page(header: none, height: auto, margins: (top: 15pt, bottom: 25pt))
|
||||
The END.
|
||||
|
@ -24,7 +24,7 @@
|
||||
// Test page fill.
|
||||
#set page(width: 80pt, height: 40pt, fill: eastern)
|
||||
#text(15pt, "Roboto", fill: white, smallcaps: true)[Typst]
|
||||
#page(width: 40pt, fill: none, margins: auto, top: 10pt)[Hi]
|
||||
#page(width: 40pt, fill: none, margins: (top: 10pt, rest: auto))[Hi]
|
||||
|
||||
---
|
||||
// Just page followed by pagebreak.
|
||||
|
@ -16,17 +16,17 @@
|
||||
// but the B should be center-aligned.
|
||||
#set par(align: center)
|
||||
#par(align: right)[
|
||||
A #rect(width: 2cm, fill: conifer, padding: 4pt)[B]
|
||||
A #rect(width: 2cm, fill: conifer, inset: 4pt)[B]
|
||||
]
|
||||
|
||||
---
|
||||
// The inner rectangle should also be yellow here.
|
||||
// (and therefore invisible)
|
||||
[#set rect(fill: yellow);#text(1em, rect(padding: 5pt, rect()))]
|
||||
[#set rect(fill: yellow);#text(1em, rect(inset: 5pt, rect()))]
|
||||
|
||||
---
|
||||
// The inner rectangle should not be yellow here.
|
||||
A #rect(fill: yellow, padding: 5pt, rect()) B
|
||||
A #rect(fill: yellow, inset: 5pt, rect()) B
|
||||
|
||||
---
|
||||
// The inner list should not be indented extra.
|
||||
|
@ -12,7 +12,7 @@ use walkdir::WalkDir;
|
||||
use typst::diag::Error;
|
||||
use typst::eval::{Smart, Value};
|
||||
use typst::frame::{Element, Frame};
|
||||
use typst::geom::{Length, RgbaColor};
|
||||
use typst::geom::{Length, RgbaColor, Sides};
|
||||
use typst::library::layout::PageNode;
|
||||
use typst::library::text::{TextNode, TextSize};
|
||||
use typst::loading::FsLoader;
|
||||
@ -64,10 +64,10 @@ fn main() {
|
||||
let mut styles = StyleMap::new();
|
||||
styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0).into()));
|
||||
styles.set(PageNode::HEIGHT, Smart::Auto);
|
||||
styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(
|
||||
PageNode::MARGINS,
|
||||
Sides::splat(Some(Smart::Custom(Length::pt(10.0).into()))),
|
||||
);
|
||||
styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into()));
|
||||
|
||||
// Hook up an assert function into the global scope.
|
||||
|
Loading…
x
Reference in New Issue
Block a user