diff options
Diffstat (limited to 'library/src/visualize')
| -rw-r--r-- | library/src/visualize/image.rs | 197 | ||||
| -rw-r--r-- | library/src/visualize/line.rs | 118 | ||||
| -rw-r--r-- | library/src/visualize/mod.rs | 45 | ||||
| -rw-r--r-- | library/src/visualize/path.rs | 211 | ||||
| -rw-r--r-- | library/src/visualize/polygon.rs | 93 | ||||
| -rw-r--r-- | library/src/visualize/shape.rs | 569 |
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) - } -} |
