diff options
Diffstat (limited to 'library/src/visualize')
| -rw-r--r-- | library/src/visualize/image.rs | 100 | ||||
| -rw-r--r-- | library/src/visualize/line.rs | 34 | ||||
| -rw-r--r-- | library/src/visualize/shape.rs | 331 |
3 files changed, 262 insertions, 203 deletions
diff --git a/library/src/visualize/image.rs b/library/src/visualize/image.rs index 5e3c7f83..fa5b70ad 100644 --- a/library/src/visualize/image.rs +++ b/library/src/visualize/image.rs @@ -1,10 +1,10 @@ use std::ffi::OsStr; +use std::path::Path; use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use crate::prelude::*; -/// # Image /// A raster or vector graphic. /// /// Supported formats are PNG, JPEG, GIF and SVG. @@ -18,62 +18,52 @@ use crate::prelude::*; /// ] /// ``` /// -/// ## Parameters -/// - path: `EcoString` (positional, required) -/// Path to an image file. -/// -/// - width: `Rel<Length>` (named) -/// The width of the image. -/// -/// - height: `Rel<Length>` (named) -/// The height of the image. -/// -/// ## Category -/// visualize -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Image +/// Category: visualize +#[node(Construct, Layout)] pub struct ImageNode { - pub image: Image, + /// Path to an image file. + #[positional] + #[required] + pub path: EcoString, + + /// The width of the image. + #[named] + #[default] pub width: Smart<Rel<Length>>, + + /// The height of the image. + #[named] + #[default] pub height: Smart<Rel<Length>>, -} -#[node] -impl ImageNode { /// How the image should adjust itself to a given area. - pub const FIT: ImageFit = ImageFit::Cover; + #[settable] + #[default(ImageFit::Cover)] + pub fit: ImageFit, +} +impl Construct for ImageNode { fn construct(vm: &Vm, args: &mut Args) -> SourceResult<Content> { let Spanned { v: path, span } = args.expect::<Spanned<EcoString>>("path to image file")?; - - let full = vm.locate(&path).at(span)?; - let buffer = vm.world().file(&full).at(span)?; - let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); - let format = match ext.to_lowercase().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!(span, "unknown image format"), - }; - - let image = Image::new(buffer, format).at(span)?; - let width = args.named("width")?.unwrap_or_default(); - let height = args.named("height")?.unwrap_or_default(); - Ok(ImageNode { image, width, height }.pack()) + let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into(); + let _ = load(vm.world(), &path).at(span)?; + let width = args.named::<Smart<Rel<Length>>>("width")?.unwrap_or_default(); + let height = args.named::<Smart<Rel<Length>>>("height")?.unwrap_or_default(); + Ok(ImageNode::new(path).with_width(width).with_height(height).pack()) } } impl Layout for ImageNode { fn layout( &self, - _: &mut Vt, + vt: &mut Vt, styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { - let sizing = Axes::new(self.width, self.height); + let image = load(vt.world(), &self.path()).unwrap(); + let sizing = Axes::new(self.width(), self.height()); let region = sizing .zip(regions.base()) .map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r))) @@ -83,8 +73,8 @@ impl Layout for ImageNode { let region_ratio = region.x / region.y; // Find out whether the image is wider or taller than the target size. - let pxw = self.image.width() as f64; - let pxh = self.image.height() as f64; + let pxw = image.width() as f64; + let pxh = image.height() as f64; let px_ratio = pxw / pxh; let wide = px_ratio > region_ratio; @@ -116,7 +106,7 @@ impl Layout for ImageNode { // the frame to the target size, center aligning the image in the // process. let mut frame = Frame::new(fitted); - frame.push(Point::zero(), Element::Image(self.image.clone(), fitted)); + frame.push(Point::zero(), Element::Image(image, fitted)); frame.resize(target, Align::CENTER_HORIZON); // Create a clipping group if only part of the image should be visible. @@ -142,7 +132,7 @@ pub enum ImageFit { Stretch, } -castable! { +cast_from_value! { ImageFit, /// The image should completely cover the area. This is the default. "cover" => Self::Cover, @@ -152,3 +142,27 @@ castable! { /// this means that the image will be distorted. "stretch" => Self::Stretch, } + +cast_to_value! { + fit: ImageFit => Value::from(match fit { + ImageFit::Cover => "cover", + ImageFit::Contain => "contain", + ImageFit::Stretch => "stretch", + }) +} + +/// Load an image from a path. +#[comemo::memoize] +fn load(world: Tracked<dyn World>, full: &str) -> StrResult<Image> { + let full = Path::new(full); + let buffer = world.file(full)?; + let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default(); + let format = match ext.to_lowercase().as_str() { + "png" => ImageFormat::Raster(RasterFormat::Png), + "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), + "gif" => ImageFormat::Raster(RasterFormat::Gif), + "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), + _ => return Err("unknown image format".into()), + }; + Image::new(buffer, format) +} diff --git a/library/src/visualize/line.rs b/library/src/visualize/line.rs index 553e06c8..0e0a272f 100644 --- a/library/src/visualize/line.rs +++ b/library/src/visualize/line.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -/// # Line /// A line from one point to another. /// /// ## Example @@ -26,20 +25,20 @@ use crate::prelude::*; /// The angle at which the line points away from the origin. Mutually /// exclusive with `end`. /// -/// ## Category -/// visualize -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Line +/// Category: visualize +#[node(Construct, Layout)] pub struct LineNode { /// Where the line starts. + #[named] + #[default] pub start: Axes<Rel<Length>>, + /// The offset from `start` where the line ends. + #[named] + #[default] pub delta: Axes<Rel<Length>>, -} -#[node] -impl LineNode { /// How to stroke the line. This can be: /// /// - A length specifying the stroke's thickness. The color is inherited, @@ -52,12 +51,16 @@ impl LineNode { /// ```example /// #line(length: 100%, stroke: 2pt + red) /// ``` - #[property(resolve, fold)] - pub const STROKE: PartialStroke = PartialStroke::default(); + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: PartialStroke, +} +impl Construct for LineNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { let start = args.named("start")?.unwrap_or_default(); - let delta = match args.named::<Axes<Rel<Length>>>("end")? { Some(end) => end.zip(start).map(|(to, from)| to - from), None => { @@ -71,8 +74,7 @@ impl LineNode { Axes::new(x, y) } }; - - Ok(Self { start, delta }.pack()) + Ok(Self::new().with_start(start).with_delta(delta).pack()) } } @@ -86,13 +88,13 @@ impl Layout for LineNode { let stroke = styles.get(Self::STROKE).unwrap_or_default(); let origin = self - .start + .start() .resolve(styles) .zip(regions.base()) .map(|(l, b)| l.relative_to(b)); let delta = self - .delta + .delta() .resolve(styles) .zip(regions.base()) .map(|(l, b)| l.relative_to(b)); diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs index e5259d91..ef81a871 100644 --- a/library/src/visualize/shape.rs +++ b/library/src/visualize/shape.rs @@ -2,7 +2,6 @@ use std::f64::consts::SQRT_2; use crate::prelude::*; -/// # Rectangle /// A rectangle with optional content. /// /// ## Example @@ -17,32 +16,28 @@ use crate::prelude::*; /// ] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional) -/// The content to place into the rectangle. -/// -/// When this is omitted, the rectangle takes on a default size of at most -/// `{45pt}` by `{30pt}`. -/// -/// - width: `Rel<Length>` (named) -/// The rectangle's width, relative to its parent container. -/// -/// - height: `Rel<Length>` (named) -/// The rectangle's height, relative to its parent container. -/// -/// ## Category -/// visualize -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Rectangle +/// Category: visualize +#[node(Layout)] pub struct RectNode { + /// 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] + #[default] pub body: Option<Content>, + + /// The rectangle's width, relative to its parent container. + #[named] + #[default] pub width: Smart<Rel<Length>>, + + /// The rectangle's height, relative to its parent container. + #[named] + #[default] pub height: Smart<Rel<Length>>, -} -#[node] -impl RectNode { /// How to fill the rectangle. /// /// When setting a fill, the default stroke disappears. To create a @@ -51,7 +46,9 @@ impl RectNode { /// ```example /// #rect(fill: blue) /// ``` - pub const FILL: Option<Paint> = None; + #[settable] + #[default] + pub fill: Option<Paint>, /// How to stroke the rectangle. This can be: /// @@ -85,8 +82,11 @@ impl RectNode { /// rect(stroke: 2pt + red), /// ) /// ``` - #[property(resolve, fold)] - pub const STROKE: Smart<Sides<Option<Option<PartialStroke>>>> = Smart::Auto; + #[settable] + #[resolve] + #[fold] + #[default] + 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: @@ -122,8 +122,11 @@ impl RectNode { /// ), /// ) /// ``` - #[property(resolve, fold)] - pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub radius: Corners<Option<Rel<Length>>>, /// How much to pad the rectangle's content. /// @@ -135,20 +138,19 @@ impl RectNode { /// ```example /// #rect(inset: 0pt)[Tight]) /// ``` - #[property(resolve, fold)] - pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[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. - #[property(resolve, fold)] - pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let width = args.named("width")?.unwrap_or_default(); - let height = args.named("height")?.unwrap_or_default(); - let body = args.eat()?; - Ok(Self { body, width, height }.pack()) - } + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides<Option<Rel<Length>>>, } impl Layout for RectNode { @@ -163,8 +165,8 @@ impl Layout for RectNode { styles, regions, ShapeKind::Rect, - &self.body, - Axes::new(self.width, self.height), + &self.body(), + Axes::new(self.width(), self.height()), styles.get(Self::FILL), styles.get(Self::STROKE), styles.get(Self::INSET), @@ -174,7 +176,6 @@ impl Layout for RectNode { } } -/// # Square /// A square with optional content. /// /// ## Example @@ -189,69 +190,77 @@ impl Layout for RectNode { /// ] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional) -/// 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}`. -/// -/// - size: `Length` (named) -/// The square's side length. This is mutually exclusive with `width` and -/// `height`. -/// -/// - width: `Rel<Length>` (named) -/// 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. -/// -/// - height: `Rel<Length>` (named) -/// 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. -/// -/// ## Category -/// visualize -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Square +/// Category: visualize +#[node(Construct, Layout)] pub struct SquareNode { + /// 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] + #[default] pub body: Option<Content>, + + /// 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. + #[named] + #[default] 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. + #[named] + #[default] pub height: Smart<Rel<Length>>, -} -#[node] -impl SquareNode { /// How to fill the square. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option<Paint> = None; + #[settable] + #[default] + pub fill: Option<Paint>, /// How to stroke the square. See the [rectangle's /// documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Smart<Sides<Option<Option<PartialStroke>>>> = Smart::Auto; + #[settable] + #[resolve] + #[fold] + #[default] + 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. - #[property(resolve, fold)] - pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + 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. /// /// The default value is `{5pt}`. - #[property(resolve, fold)] - pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[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. - #[property(resolve, fold)] - pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides<Option<Rel<Length>>>, +} +impl Construct for SquareNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from)); let width = match size { @@ -264,8 +273,12 @@ impl SquareNode { size => size, } .unwrap_or_default(); - let body = args.eat()?; - Ok(Self { body, width, height }.pack()) + let body = args.eat::<Content>()?; + Ok(Self::new() + .with_body(body) + .with_width(width) + .with_height(height) + .pack()) } } @@ -281,8 +294,8 @@ impl Layout for SquareNode { styles, regions, ShapeKind::Square, - &self.body, - Axes::new(self.width, self.height), + &self.body(), + Axes::new(self.width(), self.height()), styles.get(Self::FILL), styles.get(Self::STROKE), styles.get(Self::INSET), @@ -292,7 +305,6 @@ impl Layout for SquareNode { } } -/// # Ellipse /// An ellipse with optional content. /// /// ## Example @@ -308,59 +320,59 @@ impl Layout for SquareNode { /// ] /// ``` /// -/// ## Parameters -/// - body: `Content` (positional) -/// The content to place into the ellipse. -/// -/// When this is omitted, the ellipse takes on a default size of at most -/// `{45pt}` by `{30pt}`. -/// -/// - width: `Rel<Length>` (named) -/// The ellipse's width, relative to its parent container. -/// -/// - height: `Rel<Length>` (named) -/// The ellipse's height, relative to its parent container. -/// -/// ## Category -/// visualize -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Ellipse +/// Category: visualize +#[node(Layout)] pub struct EllipseNode { + /// 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] + #[default] pub body: Option<Content>, + + /// The ellipse's width, relative to its parent container. + #[named] + #[default] pub width: Smart<Rel<Length>>, + + /// The ellipse's height, relative to its parent container. + #[named] + #[default] pub height: Smart<Rel<Length>>, -} -#[node] -impl EllipseNode { /// How to fill the ellipse. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option<Paint> = None; + #[settable] + #[default] + pub fill: Option<Paint>, /// How to stroke the ellipse. See the [rectangle's /// documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Smart<Option<PartialStroke>> = Smart::Auto; + #[settable] + #[resolve] + #[fold] + #[default] + pub stroke: Smart<Option<PartialStroke>>, /// How much to pad the ellipse's content. See the [rectangle's /// documentation]($func/rect.inset) for more details. /// /// The default value is `{5pt}`. - #[property(resolve, fold)] - pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[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. - #[property(resolve, fold)] - pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); - - fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { - let width = args.named("width")?.unwrap_or_default(); - let height = args.named("height")?.unwrap_or_default(); - let body = args.eat()?; - Ok(Self { body, width, height }.pack()) - } + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides<Option<Rel<Length>>>, } impl Layout for EllipseNode { @@ -375,8 +387,8 @@ impl Layout for EllipseNode { styles, regions, ShapeKind::Ellipse, - &self.body, - Axes::new(self.width, self.height), + &self.body(), + Axes::new(self.width(), self.height()), styles.get(Self::FILL), styles.get(Self::STROKE).map(Sides::splat), styles.get(Self::INSET), @@ -386,7 +398,6 @@ impl Layout for EllipseNode { } } -/// # Circle /// A circle with optional content. /// /// ## Example @@ -423,40 +434,68 @@ impl Layout for EllipseNode { /// In contrast to `size`, this can be relative to the parent container's /// height. /// -/// ## Category -/// visualize -#[func] -#[capable(Layout)] -#[derive(Debug, Hash)] +/// Display: Circle +/// Category: visualize +#[node(Construct, Layout)] pub struct CircleNode { + /// The content to place into the circle. The circle expands to fit this + /// content, keeping the 1-1 aspect ratio. + #[positional] + #[default] pub body: Option<Content>, + + /// The circle's width. This is mutually exclusive with `radius` and + /// `height`. + /// + /// In contrast to `size`, this can be relative to the parent container's + /// width. + #[named] + #[default] pub width: Smart<Rel<Length>>, + + /// The circle's height.This is mutually exclusive with `radius` and + /// `width`. + /// + /// In contrast to `size`, this can be relative to the parent container's + /// height. + #[named] + #[default] pub height: Smart<Rel<Length>>, -} -#[node] -impl CircleNode { /// How to fill the circle. See the /// [rectangle's documentation]($func/rect.fill) for more details. - pub const FILL: Option<Paint> = None; + #[settable] + #[default] + pub fill: Option<Paint>, /// How to stroke the circle. See the [rectangle's /// documentation]($func/rect.stroke) for more details. - #[property(resolve, fold)] - pub const STROKE: Smart<Option<PartialStroke>> = Smart::Auto; + #[settable] + #[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. /// /// The default value is `{5pt}`. - #[property(resolve, fold)] - pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); + #[settable] + #[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. - #[property(resolve, fold)] - pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); + #[settable] + #[resolve] + #[fold] + #[default] + pub outset: Sides<Option<Rel<Length>>>, +} +impl Construct for CircleNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { let size = args .named::<Smart<Length>>("radius")? @@ -471,8 +510,12 @@ impl CircleNode { size => size, } .unwrap_or_default(); - let body = args.eat()?; - Ok(Self { body, width, height }.pack()) + let body = args.eat::<Content>()?; + Ok(Self::new() + .with_body(body) + .with_width(width) + .with_height(height) + .pack()) } } @@ -488,8 +531,8 @@ impl Layout for CircleNode { styles, regions, ShapeKind::Circle, - &self.body, - Axes::new(self.width, self.height), + &self.body(), + Axes::new(self.width(), self.height()), styles.get(Self::FILL), styles.get(Self::STROKE).map(Sides::splat), styles.get(Self::INSET), |
