summaryrefslogtreecommitdiff
path: root/library/src/visualize
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/visualize')
-rw-r--r--library/src/visualize/image.rs197
-rw-r--r--library/src/visualize/line.rs118
-rw-r--r--library/src/visualize/mod.rs45
-rw-r--r--library/src/visualize/path.rs211
-rw-r--r--library/src/visualize/polygon.rs93
-rw-r--r--library/src/visualize/shape.rs569
6 files changed, 0 insertions, 1233 deletions
diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs
deleted file mode 100644
index b169cc4d..00000000
--- a/library/src/visualize/image.rs
+++ /dev/null
@@ -1,197 +0,0 @@
-use std::ffi::OsStr;
-use std::path::Path;
-
-use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
-use typst::util::Bytes;
-
-use crate::meta::{Figurable, LocalName};
-use crate::prelude::*;
-use crate::text::families;
-
-/// A raster or vector graphic.
-///
-/// Supported formats are PNG, JPEG, GIF and SVG.
-///
-/// _Note:_ Work on SVG export is ongoing and there might be visual inaccuracies
-/// in the resulting PDF. Make sure to double-check embedded SVG images. If you
-/// have an issue, also feel free to report it on [GitHub][gh-svg].
-///
-/// ## Example { #example }
-/// ```example
-/// #figure(
-/// image("molecular.jpg", width: 80%),
-/// caption: [
-/// A step in the molecular testing
-/// pipeline of our lab.
-/// ],
-/// )
-/// ```
-///
-/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg
-///
-/// Display: Image
-/// Category: visualize
-#[element(Layout, LocalName, Figurable)]
-pub struct ImageElem {
- /// Path to an image file.
- #[required]
- #[parse(
- let Spanned { v: path, span } =
- args.expect::<Spanned<EcoString>>("path to image file")?;
- let id = vm.location().join(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- path
- )]
- pub path: EcoString,
-
- /// The raw file data.
- #[internal]
- #[required]
- #[parse(data)]
- pub data: Bytes,
-
- /// The width of the image.
- pub width: Smart<Rel<Length>>,
-
- /// The height of the image.
- pub height: Smart<Rel<Length>>,
-
- /// A text describing the image.
- pub alt: Option<EcoString>,
-
- /// How the image should adjust itself to a given area.
- #[default(ImageFit::Cover)]
- pub fit: ImageFit,
-}
-
-impl Layout for ImageElem {
- #[tracing::instrument(name = "ImageElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let ext = Path::new(self.path().as_str())
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default()
- .to_lowercase();
-
- let format = match ext.as_str() {
- "png" => ImageFormat::Raster(RasterFormat::Png),
- "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
- "gif" => ImageFormat::Raster(RasterFormat::Gif),
- "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
- _ => bail!(self.span(), "unknown image format"),
- };
-
- let image = Image::with_fonts(
- self.data(),
- format,
- vt.world,
- families(styles).next().as_ref().map(|f| f.as_str()),
- self.alt(styles),
- )
- .at(self.span())?;
-
- let sizing = Axes::new(self.width(styles), self.height(styles));
- let region = sizing
- .zip(regions.base())
- .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r)))
- .unwrap_or(regions.base());
-
- let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand;
- let region_ratio = region.x / region.y;
-
- // Find out whether the image is wider or taller than the target size.
- let pxw = image.width() as f64;
- let pxh = image.height() as f64;
- let px_ratio = pxw / pxh;
- let wide = px_ratio > region_ratio;
-
- // The space into which the image will be placed according to its fit.
- let target = if expand.x && expand.y {
- region
- } else if expand.x || (!expand.y && wide && region.x.is_finite()) {
- Size::new(region.x, region.y.min(region.x.safe_div(px_ratio)))
- } else if region.y.is_finite() {
- Size::new(region.x.min(region.y * px_ratio), region.y)
- } else {
- Size::new(Abs::pt(pxw), Abs::pt(pxh))
- };
-
- // Compute the actual size of the fitted image.
- let fit = self.fit(styles);
- let fitted = match fit {
- ImageFit::Cover | ImageFit::Contain => {
- if wide == (fit == ImageFit::Contain) {
- Size::new(target.x, target.x / px_ratio)
- } else {
- Size::new(target.y * px_ratio, target.y)
- }
- }
- ImageFit::Stretch => target,
- };
-
- // First, place the image in a frame of exactly its size and then resize
- // the frame to the target size, center aligning the image in the
- // process.
- let mut frame = Frame::new(fitted);
- frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
- frame.resize(target, Align::CENTER_HORIZON);
-
- // Create a clipping group if only part of the image should be visible.
- if fit == ImageFit::Cover && !target.fits(fitted) {
- frame.clip();
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
- }
-}
-
-impl LocalName for ImageElem {
- fn local_name(&self, lang: Lang, _: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Figurë",
- Lang::ARABIC => "شكل",
- Lang::BOKMÅL => "Figur",
- Lang::CHINESE => "图",
- Lang::CZECH => "Obrázek",
- Lang::DANISH => "Figur",
- Lang::DUTCH => "Figuur",
- Lang::FILIPINO => "Pigura",
- Lang::FRENCH => "Figure",
- Lang::GERMAN => "Abbildung",
- Lang::ITALIAN => "Figura",
- Lang::NYNORSK => "Figur",
- Lang::POLISH => "Rysunek",
- Lang::PORTUGUESE => "Figura",
- Lang::RUSSIAN => "Рисунок",
- Lang::SLOVENIAN => "Slika",
- Lang::SPANISH => "Figura",
- Lang::SWEDISH => "Figur",
- Lang::TURKISH => "Şekil",
- Lang::UKRAINIAN => "Рисунок",
- Lang::VIETNAMESE => "Hình",
- Lang::ENGLISH | _ => "Figure",
- }
- }
-}
-
-impl Figurable for ImageElem {}
-
-/// How an image should adjust itself to a given area.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum ImageFit {
- /// The image should completely cover the area. This is the default.
- Cover,
- /// The image should be fully contained in the area.
- Contain,
- /// The image should be stretched so that it exactly fills the area, even if
- /// this means that the image will be distorted.
- Stretch,
-}
diff --git a/library/src/visualize/line.rs b/library/src/visualize/line.rs
deleted file mode 100644
index 62a381a9..00000000
--- a/library/src/visualize/line.rs
+++ /dev/null
@@ -1,118 +0,0 @@
-use crate::prelude::*;
-
-/// A line from one point to another.
-///
-/// ## Example { #example }
-/// ```example
-/// #set page(height: 100pt)
-///
-/// #line(length: 100%)
-/// #line(end: (50%, 50%))
-/// #line(
-/// length: 4cm,
-/// stroke: 2pt + maroon,
-/// )
-/// ```
-///
-/// Display: Line
-/// Category: visualize
-#[element(Layout)]
-pub struct LineElem {
- /// The start point of the line.
- ///
- /// Must be an array of exactly two relative lengths.
- #[resolve]
- pub start: Axes<Rel<Length>>,
-
- /// The offset from `start` where the line ends.
- #[resolve]
- pub end: Option<Axes<Rel<Length>>>,
-
- /// The line's length. This is only respected if `end` is `none`.
- #[resolve]
- #[default(Abs::pt(30.0).into())]
- pub length: Rel<Length>,
-
- /// The angle at which the line points away from the origin. This is only
- /// respected if `end` is `none`.
- pub angle: Angle,
-
- /// How to stroke the line. This can be:
- ///
- /// - A length specifying the stroke's thickness. The color is inherited,
- /// defaulting to black.
- /// - A color to use for the stroke. The thickness is inherited, defaulting
- /// to `{1pt}`.
- /// - A stroke combined from color and thickness using the `+` operator as
- /// in `{2pt + red}`.
- /// - A stroke described by a dictionary with any of the following keys:
- /// - `paint`: The [color]($type/color) to use for the stroke.
- /// - `thickness`: The stroke's thickness as a [length]($type/length).
- /// - `cap`: How the line terminates. One of `{"butt"}`, `{"round"}`, or
- /// `{"square"}`.
- /// - `join`: How sharp turns of a contour are rendered. One of
- /// `{"miter"}`, `{"round"}`, or `{"bevel"}`. Not applicable to lines
- /// but to [polygons]($func/polygon) or [paths]($func/path).
- /// - `miter-limit`: Number at which protruding sharp angles are rendered
- /// with a bevel instead. The higher the number, the sharper an angle
- /// can be before it is bevelled. Only applicable if `join` is
- /// `{"miter"}`. Defaults to `{4.0}`.
- /// - `dash`: The dash pattern to use. Can be any of the following:
- /// - One of the predefined patterns `{"solid"}`, `{"dotted"}`,
- /// `{"densely-dotted"}`, `{"loosely-dotted"}`, `{"dashed"}`,
- /// `{"densely-dashed"}`, `{"loosely-dashed"}`, `{"dash-dotted"}`,
- /// `{"densely-dash-dotted"}` or `{"loosely-dash-dotted"}`
- /// - An [array]($type/array) with alternating lengths for dashes and
- /// gaps. You can also use the string `{"dot"}` for a length equal to
- /// the line thickness.
- /// - A [dictionary]($type/dictionary) with the keys `array` (same as
- /// the array above), and `phase` (of type [length]($type/length)),
- /// which defines where in the pattern to start drawing.
- ///
- /// ```example
- /// #set line(length: 100%)
- /// #stack(
- /// spacing: 1em,
- /// line(stroke: 2pt + red),
- /// line(stroke: (paint: blue, thickness: 4pt, cap: "round")),
- /// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")),
- /// line(stroke: (paint: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: PartialStroke,
-}
-
-impl Layout for LineElem {
- #[tracing::instrument(name = "LineElem::layout", skip_all)]
- fn layout(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let resolve = |axes: Axes<Rel<Abs>>| {
- axes.zip(regions.base()).map(|(l, b)| l.relative_to(b))
- };
-
- let start = resolve(self.start(styles));
- let delta =
- self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
- let length = self.length(styles);
- let angle = self.angle(styles);
- let x = angle.cos() * length;
- let y = angle.sin() * length;
- resolve(Axes::new(x, y))
- });
-
- let stroke = self.stroke(styles).unwrap_or_default();
- let size = start.max(start + delta).max(Size::zero());
- let target = regions.expand.select(regions.size, size);
-
- let mut frame = Frame::new(target);
- let shape = Geometry::Line(delta.to_point()).stroked(stroke);
- frame.push(start.to_point(), FrameItem::Shape(shape, self.span()));
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/library/src/visualize/mod.rs b/library/src/visualize/mod.rs
deleted file mode 100644
index ea873f44..00000000
--- a/library/src/visualize/mod.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-//! Drawing and visualization.
-
-mod image;
-mod line;
-mod path;
-mod polygon;
-mod shape;
-
-pub use self::image::*;
-pub use self::line::*;
-pub use self::path::*;
-pub use self::polygon::*;
-pub use self::shape::*;
-
-use crate::prelude::*;
-
-/// Hook up all visualize definitions.
-pub(super) fn define(global: &mut Scope) {
- global.define("image", ImageElem::func());
- global.define("line", LineElem::func());
- global.define("rect", RectElem::func());
- global.define("square", SquareElem::func());
- global.define("ellipse", EllipseElem::func());
- global.define("circle", CircleElem::func());
- global.define("polygon", PolygonElem::func());
- global.define("path", PathElem::func());
- global.define("black", Color::BLACK);
- global.define("gray", Color::GRAY);
- global.define("silver", Color::SILVER);
- global.define("white", Color::WHITE);
- global.define("navy", Color::NAVY);
- global.define("blue", Color::BLUE);
- global.define("aqua", Color::AQUA);
- global.define("teal", Color::TEAL);
- global.define("eastern", Color::EASTERN);
- global.define("purple", Color::PURPLE);
- global.define("fuchsia", Color::FUCHSIA);
- global.define("maroon", Color::MAROON);
- global.define("red", Color::RED);
- global.define("orange", Color::ORANGE);
- global.define("yellow", Color::YELLOW);
- global.define("olive", Color::OLIVE);
- global.define("green", Color::GREEN);
- global.define("lime", Color::LIME);
-}
diff --git a/library/src/visualize/path.rs b/library/src/visualize/path.rs
deleted file mode 100644
index d78abce1..00000000
--- a/library/src/visualize/path.rs
+++ /dev/null
@@ -1,211 +0,0 @@
-use kurbo::{CubicBez, ParamCurveExtrema};
-use typst::eval::Reflect;
-
-use crate::prelude::*;
-
-use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
-
-/// A path through a list of points, connected by Bezier curves.
-///
-/// ## Example { #example }
-/// ```example
-/// #path(
-/// fill: blue.lighten(80%),
-/// stroke: blue,
-/// closed: true,
-/// (0pt, 50pt),
-/// (100%, 50pt),
-/// ((50%, 0pt), (40pt, 0pt)),
-/// )
-/// ```
-///
-/// Display: Path
-/// Category: visualize
-#[element(Layout)]
-pub struct PathElem {
- /// How to fill the path. See the
- /// [rectangle's documentation]($func/rect.fill) for more details.
- ///
- /// Currently all paths are filled according to the
- /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
- pub fill: Option<Paint>,
-
- /// How to stroke the path. This can be:
- ///
- /// See the [line's documentation]($func/line.stroke) for more details. Can
- /// be set to `{none}` to disable the stroke or to `{auto}` for a stroke of
- /// `{1pt}` black if and if only if no fill is given.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<PartialStroke>>,
-
- /// Whether to close this path with one last bezier curve. This curve will
- /// takes into account the adjacent control points. If you want to close
- /// with a straight line, simply add one last point that's the same as the
- /// start point.
- #[default(false)]
- pub closed: bool,
-
- /// The vertices of the path.
- ///
- /// Each vertex can be defined in 3 ways:
- ///
- /// - A regular point, as given to the [`line`]($func/line) or
- /// [`polygon`]($func/polygon) function.
- /// - An array of two points, the first being the vertex and the second
- /// being the control point. The control point is expressed relative to
- /// the vertex and is mirrored to get the second control point. The given
- /// control point is the one that affects the curve coming _into_ this
- /// vertex (even for the first point). The mirrored control point affects
- /// the curve going out of this vertex.
- /// - An array of three points, the first being the vertex and the next
- /// being the control points (control point for curves coming in and out,
- /// respectively)
- #[variadic]
- pub vertices: Vec<PathVertex>,
-}
-
-impl Layout for PathElem {
- #[tracing::instrument(name = "PathElem::layout", skip_all)]
- fn layout(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let resolve = |axes: Axes<Rel<Length>>| {
- axes.resolve(styles)
- .zip(regions.base())
- .map(|(l, b)| l.relative_to(b))
- .to_point()
- };
-
- let vertices: Vec<PathVertex> = self.vertices();
- let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
-
- let mut size = Size::zero();
- if points.is_empty() {
- return Ok(Fragment::frame(Frame::new(size)));
- }
-
- // Only create a path if there are more than zero points.
- // Construct a closed path given all points.
- let mut path = Path::new();
- path.move_to(points[0]);
-
- let mut add_cubic =
- |from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| {
- let from_control_point = resolve(from.control_point_from()) + from_point;
- let to_control_point = resolve(to.control_point_to()) + to_point;
- path.cubic_to(from_control_point, to_control_point, to_point);
-
- let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
- let p1 = kurbo::Point::new(
- from_control_point.x.to_raw(),
- from_control_point.y.to_raw(),
- );
- let p2 = kurbo::Point::new(
- to_control_point.x.to_raw(),
- to_control_point.y.to_raw(),
- );
- let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
- let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
- size.x.set_max(Abs::raw(extrema.x1));
- size.y.set_max(Abs::raw(extrema.y1));
- };
-
- for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
- let from = vertex_window[0];
- let to = vertex_window[1];
- let from_point = point_window[0];
- let to_point = point_window[1];
-
- add_cubic(from_point, to_point, from, to);
- }
-
- if self.closed(styles) {
- let from = *vertices.last().unwrap(); // We checked that we have at least one element.
- let to = vertices[0];
- let from_point = *points.last().unwrap();
- let to_point = points[0];
-
- add_cubic(from_point, to_point, from, to);
- path.close_path();
- }
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = match self.stroke(styles) {
- Smart::Auto if fill.is_none() => Some(Stroke::default()),
- Smart::Auto => None,
- Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default),
- };
-
- let mut frame = Frame::new(size);
- let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
- frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
-
- Ok(Fragment::frame(frame))
- }
-}
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum PathVertex {
- Vertex(Axes<Rel<Length>>),
- MirroredControlPoint(Axes<Rel<Length>>, Axes<Rel<Length>>),
- AllControlPoints(Axes<Rel<Length>>, Axes<Rel<Length>>, Axes<Rel<Length>>),
-}
-
-impl PathVertex {
- pub fn vertex(&self) -> Axes<Rel<Length>> {
- match self {
- Vertex(x) => *x,
- MirroredControlPoint(x, _) => *x,
- AllControlPoints(x, _, _) => *x,
- }
- }
-
- pub fn control_point_from(&self) -> Axes<Rel<Length>> {
- match self {
- Vertex(_) => Axes::new(Rel::zero(), Rel::zero()),
- MirroredControlPoint(_, a) => a.map(|x| -x),
- AllControlPoints(_, _, b) => *b,
- }
- }
-
- pub fn control_point_to(&self) -> Axes<Rel<Length>> {
- match self {
- Vertex(_) => Axes::new(Rel::zero(), Rel::zero()),
- MirroredControlPoint(_, a) => *a,
- AllControlPoints(_, a, _) => *a,
- }
- }
-}
-
-cast! {
- PathVertex,
- self => match self {
- Vertex(x) => x.into_value(),
- MirroredControlPoint(x, c) => array![x, c].into_value(),
- AllControlPoints(x, c1, c2) => array![x, c1, c2].into_value(),
- },
- array: Array => {
- let mut iter = array.into_iter();
- match (iter.next(), iter.next(), iter.next(), iter.next()) {
- (Some(a), None, None, None) => {
- Vertex(a.cast()?)
- },
- (Some(a), Some(b), None, None) => {
- if Axes::<Rel<Length>>::castable(&a) {
- MirroredControlPoint(a.cast()?, b.cast()?)
- } else {
- Vertex(Axes::new(a.cast()?, b.cast()?))
- }
- },
- (Some(a), Some(b), Some(c), None) => {
- AllControlPoints(a.cast()?, b.cast()?, c.cast()?)
- },
- _ => bail!("path vertex must have 1, 2, or 3 points"),
- }
- },
-}
diff --git a/library/src/visualize/polygon.rs b/library/src/visualize/polygon.rs
deleted file mode 100644
index d2d3132a..00000000
--- a/library/src/visualize/polygon.rs
+++ /dev/null
@@ -1,93 +0,0 @@
-use crate::prelude::*;
-
-/// A closed polygon.
-///
-/// The polygon is defined by its corner points and is closed automatically.
-///
-/// ## Example { #example }
-/// ```example
-/// #polygon(
-/// fill: blue.lighten(80%),
-/// stroke: blue,
-/// (20%, 0pt),
-/// (60%, 0pt),
-/// (80%, 2cm),
-/// (0%, 2cm),
-/// )
-/// ```
-///
-/// Display: Polygon
-/// Category: visualize
-#[element(Layout)]
-pub struct PolygonElem {
- /// How to fill the polygon. See the
- /// [rectangle's documentation]($func/rect.fill) for more details.
- ///
- /// Currently all polygons are filled according to the
- /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
- pub fill: Option<Paint>,
-
- /// How to stroke the polygon. This can be:
- ///
- /// See the [line's documentation]($func/line.stroke) for more details. Can
- /// be set to `{none}` to disable the stroke or to `{auto}` for a stroke of
- /// `{1pt}` black if and if only if no fill is given.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<PartialStroke>>,
-
- /// The vertices of the polygon. Each point is specified as an array of two
- /// [relative lengths]($type/relative-length).
- #[variadic]
- pub vertices: Vec<Axes<Rel<Length>>>,
-}
-
-impl Layout for PolygonElem {
- #[tracing::instrument(name = "PolygonElem::layout", skip_all)]
- fn layout(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let points: Vec<Point> = self
- .vertices()
- .iter()
- .map(|c| {
- c.resolve(styles)
- .zip(regions.base())
- .map(|(l, b)| l.relative_to(b))
- .to_point()
- })
- .collect();
-
- let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
- let mut frame = Frame::new(size);
-
- // Only create a path if there are more than zero points.
- if points.is_empty() {
- return Ok(Fragment::frame(frame));
- }
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = match self.stroke(styles) {
- Smart::Auto if fill.is_none() => Some(Stroke::default()),
- Smart::Auto => None,
- Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default),
- };
-
- // Construct a closed path given all points.
- let mut path = Path::new();
- path.move_to(points[0]);
- for &point in &points[1..] {
- path.line_to(point);
- }
- path.close_path();
-
- let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
- frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
-
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs
deleted file mode 100644
index 6129b70b..00000000
--- a/library/src/visualize/shape.rs
+++ /dev/null
@@ -1,569 +0,0 @@
-use std::f64::consts::SQRT_2;
-
-use crate::prelude::*;
-
-/// A rectangle with optional content.
-///
-/// ## Example { #example }
-/// ```example
-/// // Without content.
-/// #rect(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #rect[
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-///
-/// Display: Rectangle
-/// Category: visualize
-#[element(Layout)]
-pub struct RectElem {
- /// The rectangle's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The rectangle's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the rectangle.
- ///
- /// When setting a fill, the default stroke disappears. To create a
- /// rectangle with both fill and stroke, you have to configure both.
- ///
- /// ```example
- /// #rect(fill: blue)
- /// ```
- pub fill: Option<Paint>,
-
- /// How to stroke the rectangle. This can be:
- ///
- /// - `{none}` to disable stroking
- /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
- /// given.
- /// - Any kind of stroke that can also be used for
- /// [lines]($func/line.stroke).
- /// - A dictionary describing the stroke for each side inidvidually. The
- /// dictionary can contain the following keys in order of precedence:
- /// - `top`: The top stroke.
- /// - `right`: The right stroke.
- /// - `bottom`: The bottom stroke.
- /// - `left`: The left stroke.
- /// - `x`: The horizontal stroke.
- /// - `y`: The vertical stroke.
- /// - `rest`: The stroke on all sides except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #stack(
- /// dir: ltr,
- /// spacing: 1fr,
- /// rect(stroke: red),
- /// rect(stroke: 2pt),
- /// rect(stroke: 2pt + red),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
-
- /// How much to round the rectangle's corners, relative to the minimum of
- /// the width and height divided by two. This can be:
- ///
- /// - A relative length for a uniform corner radius.
- /// - A dictionary: With a dictionary, the stroke for each side can be set
- /// individually. The dictionary can contain the following keys in order
- /// of precedence:
- /// - `top-left`: The top-left corner radius.
- /// - `top-right`: The top-right corner radius.
- /// - `bottom-right`: The bottom-right corner radius.
- /// - `bottom-left`: The bottom-left corner radius.
- /// - `left`: The top-left and bottom-left corner radii.
- /// - `top`: The top-left and top-right corner radii.
- /// - `right`: The top-right and bottom-right corner radii.
- /// - `bottom`: The bottom-left and bottom-right corner radii.
- /// - `rest`: The radii for all corners except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #set rect(stroke: 4pt)
- /// #rect(
- /// radius: (
- /// left: 5pt,
- /// top-right: 20pt,
- /// bottom-right: 10pt,
- /// ),
- /// stroke: (
- /// left: red,
- /// top: yellow,
- /// right: green,
- /// bottom: blue,
- /// ),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub radius: Corners<Option<Rel<Length>>>,
-
- /// How much to pad the rectangle's content.
- ///
- /// _Note:_ When the rectangle contains text, its exact size depends on the
- /// current [text edges]($func/text.top-edge).
- ///
- /// ```example
- /// #rect(inset: 0pt)[Tight]
- /// ```
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the rectangle's size without affecting the layout.
- /// See the [box's documentation]($func/box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the rectangle.
- ///
- /// When this is omitted, the rectangle takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for RectElem {
- #[tracing::instrument(name = "RectElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Rect,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// A square with optional content.
-///
-/// ## Example { #example }
-/// ```example
-/// // Without content.
-/// #square(size: 40pt)
-///
-/// // With content.
-/// #square[
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-///
-/// Display: Square
-/// Category: visualize
-#[element(Layout)]
-pub struct SquareElem {
- /// The square's side length. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub size: Smart<Length>,
-
- /// The square's width. This is mutually exclusive with `size` and `height`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The square's height. This is mutually exclusive with `size` and `width`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the square. See the
- /// [rectangle's documentation]($func/rect.fill) for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the square. See the [rectangle's
- /// documentation]($func/rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
-
- /// How much to round the square's corners. See the [rectangle's
- /// documentation]($func/rect.radius) for more details.
- #[resolve]
- #[fold]
- pub radius: Corners<Option<Rel<Length>>>,
-
- /// How much to pad the square's content. See the [rectangle's
- /// documentation]($func/rect.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the square's size without affecting the layout. See
- /// the [rectangle's documentation]($func/rect.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the square. The square expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- ///
- /// When this is omitted, the square takes on a default size of at most
- /// `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for SquareElem {
- #[tracing::instrument(name = "SquareElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Square,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// An ellipse with optional content.
-///
-/// ## Example { #example }
-/// ```example
-/// // Without content.
-/// #ellipse(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #ellipse[
-/// #set align(center)
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-///
-/// Display: Ellipse
-/// Category: visualize
-#[element(Layout)]
-pub struct EllipseElem {
- /// The ellipse's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The ellipse's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the ellipse. See the
- /// [rectangle's documentation]($func/rect.fill) for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the ellipse. See the [rectangle's
- /// documentation]($func/rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<PartialStroke>>,
-
- /// How much to pad the ellipse's content. See the [rectangle's
- /// documentation]($func/rect.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the ellipse's size without affecting the layout. See
- /// the [rectangle's documentation]($func/rect.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the ellipse.
- ///
- /// When this is omitted, the ellipse takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for EllipseElem {
- #[tracing::instrument(name = "EllipseElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Ellipse,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// A circle with optional content.
-///
-/// ## Example { #example }
-/// ```example
-/// // Without content.
-/// #circle(radius: 25pt)
-///
-/// // With content.
-/// #circle[
-/// #set align(center + horizon)
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-///
-/// Display: Circle
-/// Category: visualize
-#[element(Layout)]
-pub struct CircleElem {
- /// The circle's radius. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub radius: Length,
-
- /// The circle's width. This is mutually exclusive with `radius` and
- /// `height`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args
- .named::<Smart<Length>>("radius")?
- .map(|s| s.map(|r| 2.0 * Rel::from(r)));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The circle's height.This is mutually exclusive with `radius` and
- /// `width`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the circle. See the
- /// [rectangle's documentation]($func/rect.fill) for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the circle. See the [rectangle's
- /// documentation]($func/rect.stroke) for more details.
- #[resolve]
- #[fold]
- #[default(Smart::Auto)]
- pub stroke: Smart<Option<PartialStroke>>,
-
- /// How much to pad the circle's content. See the [rectangle's
- /// documentation]($func/rect.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the circle's size without affecting the layout. See
- /// the [rectangle's documentation]($func/rect.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the circle. The circle expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for CircleElem {
- #[tracing::instrument(name = "CircleElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Circle,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// Layout a shape.
-#[tracing::instrument(name = "shape::layout", skip_all)]
-#[allow(clippy::too_many_arguments)]
-fn layout(
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- kind: ShapeKind,
- body: &Option<Content>,
- sizing: Axes<Smart<Rel<Length>>>,
- fill: Option<Paint>,
- stroke: Smart<Sides<Option<PartialStroke<Abs>>>>,
- mut inset: Sides<Rel<Abs>>,
- outset: Sides<Rel<Abs>>,
- radius: Corners<Rel<Abs>>,
- span: Span,
-) -> SourceResult<Fragment> {
- let resolved = sizing
- .zip(regions.base())
- .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r)));
-
- let mut frame;
- if let Some(child) = body {
- let region = resolved.unwrap_or(regions.base());
- if kind.is_round() {
- 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 expand = sizing.as_ref().map(Smart::is_custom);
- let pod = Regions::one(region, expand);
- frame = child.layout(vt, styles, pod)?.into_frame();
-
- // Enforce correct size.
- *frame.size_mut() = expand.select(region, frame.size());
-
- // Relayout with full expansion into square region to make sure
- // the result is really a square or circle.
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- let length = frame.size().max_by_side().min(region.min_by_side());
- let pod = Regions::one(Size::splat(length), Axes::splat(true));
- frame = child.layout(vt, styles, pod)?.into_frame();
- }
-
- // Enforce correct size again.
- *frame.size_mut() = expand.select(region, frame.size());
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- }
- } else {
- // The default size that a shape takes on if it has no child and
- // enough space.
- let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
- let mut size = resolved.unwrap_or(default.min(regions.base()));
- if kind.is_quadratic() {
- size = Size::splat(size.min_by_side());
- }
- frame = Frame::new(size);
- }
-
- // Prepare stroke.
- let stroke = match 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(PartialStroke::unwrap_or_default))
- }
- };
-
- // Add fill and/or stroke.
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- if kind.is_round() {
- let outset = outset.relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let pos = Point::new(-outset.left, -outset.top);
- let shape = ellipse(size, fill, stroke.left);
- frame.prepend(pos, FrameItem::Shape(shape, span));
- } else {
- frame.fill_and_stroke(fill, stroke, outset, radius, span);
- }
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
-}
-
-/// A category of shape.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum ShapeKind {
- /// A rectangle with equal side lengths.
- Square,
- /// A quadrilateral with four right angles.
- Rect,
- /// An ellipse with coinciding foci.
- Circle,
- /// A curve around two focal points.
- Ellipse,
-}
-
-impl ShapeKind {
- /// Whether this shape kind is curvy.
- fn is_round(self) -> bool {
- matches!(self, Self::Circle | Self::Ellipse)
- }
-
- /// Whether this shape kind has equal side length.
- fn is_quadratic(self) -> bool {
- matches!(self, Self::Square | Self::Circle)
- }
-}