summaryrefslogtreecommitdiff
path: root/src/library/graphics
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-03 11:44:53 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-03 13:35:39 +0100
commit37a7afddfaffd44cb9bc013c9506599267e08983 (patch)
tree20e7d62d3c5418baff01a21d0406b91bf3096214 /src/library/graphics
parent56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff)
Split crates
Diffstat (limited to 'src/library/graphics')
-rw-r--r--src/library/graphics/hide.rs27
-rw-r--r--src/library/graphics/image.rs120
-rw-r--r--src/library/graphics/line.rs80
-rw-r--r--src/library/graphics/mod.rs11
-rw-r--r--src/library/graphics/shape.rs201
5 files changed, 0 insertions, 439 deletions
diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs
deleted file mode 100644
index d320b06c..00000000
--- a/src/library/graphics/hide.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use crate::library::prelude::*;
-
-/// Hide content without affecting layout.
-#[derive(Debug, Hash)]
-pub struct HideNode(pub Content);
-
-#[node(LayoutInline)]
-impl HideNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl LayoutInline for HideNode {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = self.0.layout_inline(world, regions, styles)?;
- for frame in &mut frames {
- frame.clear();
- }
- Ok(frames)
- }
-}
diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs
deleted file mode 100644
index e27ea488..00000000
--- a/src/library/graphics/image.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-use std::ffi::OsStr;
-
-use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
-use crate::library::prelude::*;
-use crate::library::text::TextNode;
-
-/// Show a raster or vector graphic.
-#[derive(Debug, Hash)]
-pub struct ImageNode(pub Image);
-
-#[node(LayoutInline)]
-impl ImageNode {
- /// How the image should adjust itself to a given area.
- pub const FIT: ImageFit = ImageFit::Cover;
-
- fn construct(vm: &mut 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")?;
- let height = args.named("height")?;
-
- Ok(ImageNode(image).pack().boxed(Axes::new(width, height)))
- }
-}
-
-impl LayoutInline for ImageNode {
- fn layout_inline(
- &self,
- _: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let pxw = self.0.width() as f64;
- let pxh = self.0.height() as f64;
- let px_ratio = pxw / pxh;
-
- // Find out whether the image is wider or taller than the target size.
- let &Regions { first, expand, .. } = regions;
- let region_ratio = first.x / first.y;
- 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 {
- first
- } else if expand.x || (!expand.y && wide && first.x.is_finite()) {
- Size::new(first.x, first.y.min(first.x.safe_div(px_ratio)))
- } else if first.y.is_finite() {
- Size::new(first.x.min(first.y * px_ratio), first.y)
- } else {
- Size::new(Abs::pt(pxw), Abs::pt(pxh))
- };
-
- // Compute the actual size of the fitted image.
- let fit = styles.get(Self::FIT);
- 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(), Element::Image(self.0.clone(), fitted));
- 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 link if it exists.
- if let Some(url) = styles.get(TextNode::LINK) {
- frame.link(url.clone());
- }
-
- Ok(vec![frame])
- }
-}
-
-/// How an image should adjust itself to a given area.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum ImageFit {
- /// The image should completely cover the area.
- Cover,
- /// The image should be fully contained in the area.
- Contain,
- /// The image should be stretched so that it exactly fills the area.
- Stretch,
-}
-
-castable! {
- ImageFit,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "cover" => Self::Cover,
- "contain" => Self::Contain,
- "stretch" => Self::Stretch,
- _ => Err(r#"expected "cover", "contain" or "stretch""#)?,
- },
-}
diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs
deleted file mode 100644
index ee7813a5..00000000
--- a/src/library/graphics/line.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use crate::library::prelude::*;
-
-/// Display a line without affecting the layout.
-#[derive(Debug, Hash)]
-pub struct LineNode {
- /// Where the line starts.
- origin: Axes<Rel<Length>>,
- /// The offset from the `origin` where the line ends.
- delta: Axes<Rel<Length>>,
-}
-
-#[node(LayoutInline)]
-impl LineNode {
- /// How to stroke the line.
- #[property(resolve, fold)]
- pub const STROKE: RawStroke = RawStroke::default();
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let origin = args.named("origin")?.unwrap_or_default();
-
- let delta = match args.named::<Axes<Rel<Length>>>("to")? {
- Some(to) => to.zip(origin).map(|(to, from)| to - from),
- None => {
- let length =
- args.named::<Rel<Length>>("length")?.unwrap_or(Abs::cm(1.0).into());
-
- let angle = args.named::<Angle>("angle")?.unwrap_or_default();
- let x = angle.cos() * length;
- let y = angle.sin() * length;
-
- Axes::new(x, y)
- }
- };
-
- Ok(Self { origin, delta }.pack())
- }
-}
-
-impl LayoutInline for LineNode {
- fn layout_inline(
- &self,
- _: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let stroke = styles.get(Self::STROKE).unwrap_or_default();
-
- let origin = self
- .origin
- .resolve(styles)
- .zip(regions.base)
- .map(|(l, b)| l.relative_to(b));
-
- let delta = self
- .delta
- .resolve(styles)
- .zip(regions.base)
- .map(|(l, b)| l.relative_to(b));
-
- let target = regions.expand.select(regions.first, Size::zero());
- let mut frame = Frame::new(target);
-
- let shape = Geometry::Line(delta.to_point()).stroked(stroke);
- frame.push(origin.to_point(), Element::Shape(shape));
-
- Ok(vec![frame])
- }
-}
-
-castable! {
- Axes<Rel<Length>>,
- Expected: "array of two relative lengths",
- Value::Array(array) => {
- let mut iter = array.into_iter();
- match (iter.next(), iter.next(), iter.next()) {
- (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
- _ => Err("point array must contain exactly two entries")?,
- }
- },
-}
diff --git a/src/library/graphics/mod.rs b/src/library/graphics/mod.rs
deleted file mode 100644
index 34182121..00000000
--- a/src/library/graphics/mod.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-//! Graphical elements and effects.
-
-mod hide;
-mod image;
-mod line;
-mod shape;
-
-pub use self::image::*;
-pub use hide::*;
-pub use line::*;
-pub use shape::*;
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
deleted file mode 100644
index 4804cd68..00000000
--- a/src/library/graphics/shape.rs
+++ /dev/null
@@ -1,201 +0,0 @@
-use std::f64::consts::SQRT_2;
-
-use crate::library::prelude::*;
-use crate::library::text::TextNode;
-
-/// A sizable and fillable shape with optional content.
-#[derive(Debug, Hash)]
-pub struct ShapeNode<const S: ShapeKind>(pub Option<Content>);
-
-/// A square with optional content.
-pub type SquareNode = ShapeNode<SQUARE>;
-
-/// A rectangle with optional content.
-pub type RectNode = ShapeNode<RECT>;
-
-/// A circle with optional content.
-pub type CircleNode = ShapeNode<CIRCLE>;
-
-/// A ellipse with optional content.
-pub type EllipseNode = ShapeNode<ELLIPSE>;
-
-#[node(LayoutInline)]
-impl<const S: ShapeKind> ShapeNode<S> {
- /// How to fill the shape.
- pub const FILL: Option<Paint> = None;
- /// How to stroke the shape.
- #[property(skip, resolve, fold)]
- pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto;
-
- /// How much to pad the shape's content.
- #[property(resolve, fold)]
- pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
- /// How much to extend the shape's dimensions beyond the allocated space.
- #[property(resolve, fold)]
- pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
-
- /// How much to round the shape's corners.
- #[property(skip, resolve, fold)]
- pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let size = match S {
- SQUARE => args.named::<Length>("size")?.map(Rel::from),
- CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Rel::from(r)),
- _ => None,
- };
-
- let width = match size {
- None => args.named("width")?,
- size => size,
- };
-
- let height = match size {
- None => args.named("height")?,
- size => size,
- };
-
- Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height)))
- }
-
- fn set(...) {
- if is_round(S) {
- styles.set_opt(
- Self::STROKE,
- args.named::<Smart<Option<RawStroke>>>("stroke")?
- .map(|some| some.map(Sides::splat)),
- );
- } else {
- styles.set_opt(Self::STROKE, args.named("stroke")?);
- styles.set_opt(Self::RADIUS, args.named("radius")?);
- }
- }
-}
-
-impl<const S: ShapeKind> LayoutInline for ShapeNode<S> {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames;
- if let Some(child) = &self.0 {
- let mut inset = styles.get(Self::INSET);
- if is_round(S) {
- 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 mut pod = Regions::one(regions.first, regions.base, regions.expand);
- frames = child.layout_inline(world, &pod, styles)?;
-
- for frame in frames.iter_mut() {
- frame.apply_role(Role::GenericBlock);
- }
-
- // Relayout with full expansion into square region to make sure
- // the result is really a square or circle.
- if is_quadratic(S) {
- let length = if regions.expand.x || regions.expand.y {
- let target = regions.expand.select(regions.first, Size::zero());
- target.x.max(target.y)
- } else {
- let size = frames[0].size();
- let desired = size.x.max(size.y);
- desired.min(regions.first.x).min(regions.first.y)
- };
-
- pod.first = Size::splat(length);
- pod.expand = Axes::splat(true);
- frames = child.layout_inline(world, &pod, styles)?;
- }
- } else {
- // The default size that a shape takes on if it has no child and
- // enough space.
- let mut size = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(regions.first);
-
- if is_quadratic(S) {
- let length = if regions.expand.x || regions.expand.y {
- let target = regions.expand.select(regions.first, Size::zero());
- target.x.max(target.y)
- } else {
- size.x.min(size.y)
- };
- size = Size::splat(length);
- } else {
- size = regions.expand.select(regions.first, size);
- }
-
- frames = vec![Frame::new(size)];
- }
-
- let frame = &mut frames[0];
-
- // Add fill and/or stroke.
- let fill = styles.get(Self::FILL);
- let stroke = match styles.get(Self::STROKE) {
- Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
- Smart::Auto => Sides::splat(None),
- Smart::Custom(strokes) => {
- strokes.map(|s| s.map(RawStroke::unwrap_or_default))
- }
- };
-
- let outset = styles.get(Self::OUTSET).relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
-
- let radius = styles
- .get(Self::RADIUS)
- .map(|side| side.relative_to(size.x.min(size.y) / 2.0));
-
- let pos = Point::new(-outset.left, -outset.top);
-
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- if is_round(S) {
- let shape = ellipse(size, fill, stroke.left);
- frame.prepend(pos, Element::Shape(shape));
- } else {
- frame.prepend_multiple(
- rounded_rect(size, radius, fill, stroke)
- .into_iter()
- .map(|x| (pos, Element::Shape(x))),
- )
- }
- }
-
- // Apply link if it exists.
- if let Some(url) = styles.get(TextNode::LINK) {
- frame.link(url.clone());
- }
-
- Ok(frames)
- }
-}
-
-/// A category of shape.
-pub type ShapeKind = usize;
-
-/// A rectangle with equal side lengths.
-const SQUARE: ShapeKind = 0;
-
-/// A quadrilateral with four right angles.
-const RECT: ShapeKind = 1;
-
-/// An ellipse with coinciding foci.
-const CIRCLE: ShapeKind = 2;
-
-/// A curve around two focal points.
-const ELLIPSE: ShapeKind = 3;
-
-/// Whether a shape kind is curvy.
-fn is_round(kind: ShapeKind) -> bool {
- matches!(kind, CIRCLE | ELLIPSE)
-}
-
-/// Whether a shape kind has equal side length.
-fn is_quadratic(kind: ShapeKind) -> bool {
- matches!(kind, SQUARE | CIRCLE)
-}