diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-03-11 11:58:56 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-03-11 11:58:56 +0100 |
| commit | e6b532391deb1e30dc356c4d20dd48199f748f29 (patch) | |
| tree | 7b631414931164b9a47c9d154172195fc0e1316c /src/library/graphics | |
| parent | b71113d37a29bab5c7dc4b501c33ee9afbdb8213 (diff) | |
More restructuring
Diffstat (limited to 'src/library/graphics')
| -rw-r--r-- | src/library/graphics/hide.rs | 30 | ||||
| -rw-r--r-- | src/library/graphics/image.rs | 116 | ||||
| -rw-r--r-- | src/library/graphics/mod.rs | 11 | ||||
| -rw-r--r-- | src/library/graphics/shape.rs | 166 | ||||
| -rw-r--r-- | src/library/graphics/transform.rs | 86 |
5 files changed, 409 insertions, 0 deletions
diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs new file mode 100644 index 00000000..861a1208 --- /dev/null +++ b/src/library/graphics/hide.rs @@ -0,0 +1,30 @@ +use crate::library::prelude::*; + +/// Hide a node without affecting layout. +#[derive(Debug, Hash)] +pub struct HideNode(pub LayoutNode); + +#[class] +impl HideNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + Ok(Template::inline(Self(args.expect("body")?))) + } +} + +impl Layout for HideNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let mut frames = self.0.layout(ctx, regions, styles)?; + + // Clear the frames. + for frame in &mut frames { + *frame = Arc::new(Frame { elements: vec![], ..**frame }); + } + + Ok(frames) + } +} diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs new file mode 100644 index 00000000..66fb8f4b --- /dev/null +++ b/src/library/graphics/image.rs @@ -0,0 +1,116 @@ +use crate::diag::Error; +use crate::image::ImageId; +use crate::library::prelude::*; +use crate::library::text::TextNode; + +/// Show a raster or vector graphic. +#[derive(Debug, Hash)] +pub struct ImageNode(pub ImageId); + +#[class] +impl ImageNode { + /// How the image should adjust itself to a given area. + pub const FIT: ImageFit = ImageFit::Cover; + + fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Template> { + let path = args.expect::<Spanned<EcoString>>("path to image file")?; + let full = ctx.resolve(&path.v); + let id = ctx.images.load(&full).map_err(|err| { + Error::boxed(path.span, match err.kind() { + std::io::ErrorKind::NotFound => "file not found".into(), + _ => format!("failed to load image ({})", err), + }) + })?; + + let width = args.named("width")?; + let height = args.named("height")?; + + Ok(Template::inline( + ImageNode(id).pack().sized(Spec::new(width, height)), + )) + } +} + +impl Layout for ImageNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let img = ctx.images.get(self.0); + let pxw = img.width() as f64; + let pxh = img.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(Length::pt(pxw), Length::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, 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_ref(TextNode::LINK) { + frame.link(url); + } + + Ok(vec![Arc::new(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/mod.rs b/src/library/graphics/mod.rs new file mode 100644 index 00000000..353f09ca --- /dev/null +++ b/src/library/graphics/mod.rs @@ -0,0 +1,11 @@ +//! Graphical elements and effects. + +mod hide; +mod image; +mod shape; +mod transform; + +pub use self::image::*; +pub use hide::*; +pub use shape::*; +pub use transform::*; diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs new file mode 100644 index 00000000..8b967412 --- /dev/null +++ b/src/library/graphics/shape.rs @@ -0,0 +1,166 @@ +use std::f64::consts::SQRT_2; + +use crate::library::prelude::*; +use crate::library::text::TextNode; + +/// Place a node into a sizable and fillable shape. +#[derive(Debug, Hash)] +pub struct ShapeNode<const S: ShapeKind>(pub Option<LayoutNode>); + +/// Place a node into a square. +pub type SquareNode = ShapeNode<SQUARE>; + +/// Place a node into a rectangle. +pub type RectNode = ShapeNode<RECT>; + +/// Place a node into a circle. +pub type CircleNode = ShapeNode<CIRCLE>; + +/// Place a node into an ellipse. +pub type EllipseNode = ShapeNode<ELLIPSE>; + +#[class] +impl<const S: ShapeKind> ShapeNode<S> { + /// How to fill the shape. + pub const FILL: Option<Paint> = None; + /// How the stroke the shape. + pub const STROKE: Smart<Option<Paint>> = Smart::Auto; + /// The stroke's thickness. + pub const THICKNESS: Length = Length::pt(1.0); + /// How much to pad the shape's content. + pub const PADDING: Linear = Linear::zero(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let size = match S { + SQUARE => args.named::<Length>("size")?.map(Linear::from), + CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Linear::from(r)), + _ => None, + }; + + let width = match size { + None => args.named("width")?, + size => size, + }; + + let height = match size { + None => args.named("height")?, + size => size, + }; + + Ok(Template::inline( + Self(args.find()?).pack().sized(Spec::new(width, height)), + )) + } +} + +impl<const S: ShapeKind> Layout for ShapeNode<S> { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let mut frames; + if let Some(child) = &self.0 { + let mut padding = styles.get(Self::PADDING); + if is_round(S) { + padding.rel += Relative::new(0.5 - SQRT_2 / 4.0); + } + + // Pad the child. + let child = child.clone().padded(Sides::splat(padding)); + + let mut pod = Regions::one(regions.first, regions.base, regions.expand); + frames = child.layout(ctx, &pod, styles)?; + + // 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 = Spec::splat(true); + frames = child.layout(ctx, &pod, styles)?; + } + } else { + // The default size that a shape takes on if it has no child and + // enough space. + let mut size = + Size::new(Length::pt(45.0), Length::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![Arc::new(Frame::new(size))]; + } + + let frame = Arc::make_mut(&mut frames[0]); + + // Add fill and/or stroke. + let fill = styles.get(Self::FILL); + let thickness = styles.get(Self::THICKNESS); + let stroke = styles + .get(Self::STROKE) + .unwrap_or(fill.is_none().then(|| Color::BLACK.into())) + .map(|paint| Stroke { paint, thickness }); + + if fill.is_some() || stroke.is_some() { + let geometry = if is_round(S) { + Geometry::Ellipse(frame.size) + } else { + Geometry::Rect(frame.size) + }; + + let shape = Shape { geometry, fill, stroke }; + frame.prepend(Point::zero(), Element::Shape(shape)); + } + + // Apply link if it exists. + if let Some(url) = styles.get_ref(TextNode::LINK) { + frame.link(url); + } + + 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) +} diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs new file mode 100644 index 00000000..fafb37a4 --- /dev/null +++ b/src/library/graphics/transform.rs @@ -0,0 +1,86 @@ +use crate::geom::Transform; +use crate::library::prelude::*; + +/// Transform a node without affecting layout. +#[derive(Debug, Hash)] +pub struct TransformNode<const T: TransformKind> { + /// Transformation to apply to the contents. + pub transform: Transform, + /// The node whose contents should be transformed. + pub child: LayoutNode, +} + +/// Transform a node by translating it without affecting layout. +pub type MoveNode = TransformNode<MOVE>; + +/// Transform a node by rotating it without affecting layout. +pub type RotateNode = TransformNode<ROTATE>; + +/// Transform a node by scaling it without affecting layout. +pub type ScaleNode = TransformNode<SCALE>; + +#[class] +impl<const T: TransformKind> TransformNode<T> { + /// The origin of the transformation. + pub const ORIGIN: Spec<Option<Align>> = Spec::default(); + + fn construct(_: &mut Context, args: &mut Args) -> TypResult<Template> { + let transform = match T { + MOVE => { + let tx = args.named("x")?.unwrap_or_default(); + let ty = args.named("y")?.unwrap_or_default(); + Transform::translation(tx, ty) + } + ROTATE => { + let angle = args.named_or_find("angle")?.unwrap_or_default(); + Transform::rotation(angle) + } + SCALE | _ => { + let all = args.find()?; + let sx = args.named("x")?.or(all).unwrap_or(Relative::one()); + let sy = args.named("y")?.or(all).unwrap_or(Relative::one()); + Transform::scale(sx, sy) + } + }; + + Ok(Template::inline(Self { + transform, + child: args.expect("body")?, + })) + } +} + +impl<const T: TransformKind> Layout for TransformNode<T> { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult<Vec<Arc<Frame>>> { + let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON); + let mut frames = self.child.layout(ctx, regions, styles)?; + + for frame in &mut frames { + let Spec { x, y } = origin.zip(frame.size).map(|(o, s)| o.resolve(s)); + let transform = Transform::translation(x, y) + .pre_concat(self.transform) + .pre_concat(Transform::translation(-x, -y)); + + Arc::make_mut(frame).transform(transform); + } + + Ok(frames) + } +} + +/// Kinds of transformations. +pub type TransformKind = usize; + +/// A translation on the X and Y axes. +const MOVE: TransformKind = 0; + +/// A rotational transformation. +const ROTATE: TransformKind = 1; + +/// A scale transformation. +const SCALE: TransformKind = 2; |
