diff --git a/src/layout/background.rs b/src/layout/background.rs deleted file mode 100644 index 092822609..000000000 --- a/src/layout/background.rs +++ /dev/null @@ -1,55 +0,0 @@ -use super::*; - -/// A node that places a rectangular filled background behind its child. -#[derive(Debug)] -#[cfg_attr(feature = "layout-cache", derive(Hash))] -pub struct BackgroundNode { - /// The kind of shape to use as a background. - pub shape: BackgroundShape, - /// Background color / texture. - pub fill: Paint, - /// The child node to be filled. - pub child: LayoutNode, -} - -/// The kind of shape to use as a background. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum BackgroundShape { - Rect, - Ellipse, -} - -impl Layout for BackgroundNode { - fn layout( - &self, - ctx: &mut LayoutContext, - regions: &Regions, - ) -> Vec>> { - let mut frames = self.child.layout(ctx, regions); - - for Constrained { item: frame, .. } in &mut frames { - let (point, geometry) = match self.shape { - BackgroundShape::Rect => (Point::zero(), Geometry::Rect(frame.size)), - BackgroundShape::Ellipse => { - (frame.size.to_point() / 2.0, Geometry::Ellipse(frame.size)) - } - }; - - // Create a new frame with the background geometry and the child's - // frame. - let empty = Frame::new(frame.size, frame.baseline); - let prev = std::mem::replace(frame, Rc::new(empty)); - let new = Rc::make_mut(frame); - new.push(point, Element::Geometry(geometry, self.fill)); - new.push_frame(Point::zero(), prev); - } - - frames - } -} - -impl From for LayoutNode { - fn from(background: BackgroundNode) -> Self { - Self::new(background) - } -} diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs deleted file mode 100644 index f05286431..000000000 --- a/src/layout/fixed.rs +++ /dev/null @@ -1,99 +0,0 @@ -use decorum::N64; - -use super::*; - -/// A node that can fix its child's width and height. -#[derive(Debug)] -#[cfg_attr(feature = "layout-cache", derive(Hash))] -pub struct FixedNode { - /// The fixed width, if any. - pub width: Option, - /// The fixed height, if any. - pub height: Option, - /// The fixed aspect ratio between width and height. - /// - /// The resulting frame will satisfy `width = aspect * height`. - pub aspect: Option, - /// The child node whose size to fix. - pub child: LayoutNode, -} - -impl Layout for FixedNode { - fn layout( - &self, - ctx: &mut LayoutContext, - regions: &Regions, - ) -> Vec>> { - // Fill in width or height if aspect ratio and the other is given. - let aspect = self.aspect.map(N64::into_inner); - let width = self.width.or(self.height.zip(aspect).map(|(h, a)| a * h)); - let height = self.height.or(self.width.zip(aspect).map(|(w, a)| w / a)); - - // Resolve the linears based on the current width and height. - let mut child_size = Size::new( - width.map_or(regions.current.w, |w| w.resolve(regions.base.w)), - height.map_or(regions.current.h, |h| h.resolve(regions.base.h)), - ); - - // If width or height aren't set for an axis, the base should be - // inherited from the parent for that axis. - let child_base = Size::new( - if width.is_some() { child_size.w } else { regions.base.w }, - if height.is_some() { child_size.h } else { regions.base.h }, - ); - - // Prepare constraints. - let mut constraints = Constraints::new(regions.expand); - constraints.set_base_if_linear(regions.base, Spec::new(width, height)); - - // If the size for one axis isn't specified, the `current` size along - // that axis needs to remain the same for the result to be reusable. - if width.is_none() { - constraints.exact.x = Some(regions.current.w); - } - if height.is_none() { - constraints.exact.y = Some(regions.current.h); - } - - // Apply the aspect ratio. - if let Some(aspect) = aspect { - let width = child_size.w.min(aspect * child_size.h); - child_size = Size::new(width, width / aspect); - constraints.exact = regions.current.to_spec().map(Some); - constraints.min = Spec::splat(None); - constraints.max = Spec::splat(None); - } - - // If width or height are fixed, the child should fill the available - // space along that axis. - let child_expand = Spec::new(width.is_some(), height.is_some()); - - // Layout the child. - let mut regions = Regions::one(child_size, child_base, child_expand); - let mut frames = self.child.layout(ctx, ®ions); - - // If we have an aspect ratio and the child is content-sized, we need to - // relayout with expansion. - if let Some(aspect) = aspect { - if width.is_none() && height.is_none() { - let needed = frames[0].item.size.cap(child_size); - let width = needed.w.max(aspect * needed.h); - regions.current = Size::new(width, width / aspect); - regions.expand = Spec::splat(true); - frames = self.child.layout(ctx, ®ions); - } - } - - // Overwrite the child's constraints with ours. - assert_eq!(frames.len(), 1); - frames[0].constraints = constraints; - - frames - } -} - -impl From for LayoutNode { - fn from(fixed: FixedNode) -> Self { - Self::new(fixed) - } -} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 1ca9001d1..cd59e3d2c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,8 +1,6 @@ //! Layouting. -mod background; mod constraints; -mod fixed; mod frame; mod grid; mod image; @@ -11,14 +9,13 @@ mod incremental; mod pad; mod par; mod regions; +mod shape; mod shaping; mod stack; mod tree; pub use self::image::*; -pub use background::*; pub use constraints::*; -pub use fixed::*; pub use frame::*; pub use grid::*; #[cfg(feature = "layout-cache")] @@ -26,6 +23,7 @@ pub use incremental::*; pub use pad::*; pub use par::*; pub use regions::*; +pub use shape::*; pub use shaping::*; pub use stack::*; pub use tree::*; diff --git a/src/layout/shape.rs b/src/layout/shape.rs new file mode 100644 index 000000000..aa90707c0 --- /dev/null +++ b/src/layout/shape.rs @@ -0,0 +1,141 @@ +use std::f64::consts::SQRT_2; + +use super::*; + +/// Places its child into a sizable and fillable shape. +#[derive(Debug)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] +pub struct ShapeNode { + /// Which shape to place the child into. + pub shape: ShapeKind, + /// The width, if any. + pub width: Option, + /// The height, if any. + pub height: Option, + /// How to fill the shape, if at all. + pub fill: Option, + /// The child node to place into the shape, if any. + pub child: Option, +} + +/// The type of a 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 Layout for ShapeNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + // Resolve width and height relative to the region's base. + let width = self.width.map(|w| w.resolve(regions.base.w)); + let height = self.height.map(|h| h.resolve(regions.base.h)); + + // Generate constraints. + let constraints = { + let mut cts = Constraints::new(regions.expand); + cts.set_base_if_linear(regions.base, Spec::new(self.width, self.height)); + + // Set exact and base constraint if child is automatically sized. + if self.width.is_none() { + cts.exact.x = Some(regions.current.w); + cts.base.x = Some(regions.base.w); + } + + // Same here. + if self.height.is_none() { + cts.exact.y = Some(regions.current.h); + cts.base.y = Some(regions.base.h); + } + + cts + }; + + // Layout. + let mut frames = if let Some(child) = &self.child { + let mut node: &dyn Layout = child; + + let padded; + if matches!(self.shape, ShapeKind::Circle | ShapeKind::Ellipse) { + // Padding with this ratio ensures that a rectangular child fits + // perfectly into a circle / an ellipse. + padded = PadNode { + padding: Sides::splat(Relative::new(0.5 - SQRT_2 / 4.0).into()), + child: child.clone(), + }; + node = &padded; + } + + // The "pod" is the region into which the child will be layouted. + let mut pod = { + let size = Size::new( + width.unwrap_or(regions.current.w), + height.unwrap_or(regions.current.h), + ); + + let base = Size::new( + if width.is_some() { size.w } else { regions.base.w }, + if height.is_some() { size.h } else { regions.base.h }, + ); + + let expand = Spec::new(width.is_some(), height.is_some()); + Regions::one(size, base, expand) + }; + + // Now, layout the child. + let mut frames = node.layout(ctx, &pod); + + if matches!(self.shape, ShapeKind::Square | ShapeKind::Circle) { + // Relayout with full expansion into square region to make sure + // the result is really a square or circle. + let size = frames[0].item.size; + pod.current.w = size.w.max(size.h).min(pod.current.w); + pod.current.h = pod.current.w; + pod.expand = Spec::splat(true); + frames = node.layout(ctx, &pod); + } + + // Validate and set constraints. + assert_eq!(frames.len(), 1); + frames[0].constraints = constraints; + frames + } else { + // Resolve shape size. + let size = Size::new(width.unwrap_or_default(), height.unwrap_or_default()); + vec![Frame::new(size, size.h).constrain(constraints)] + }; + + // Add background shape if desired. + if let Some(fill) = self.fill { + let frame = Rc::make_mut(&mut frames[0].item); + let (pos, geometry) = match self.shape { + ShapeKind::Square | ShapeKind::Rect => { + (Point::zero(), Geometry::Rect(frame.size)) + } + ShapeKind::Circle | ShapeKind::Ellipse => { + (frame.size.to_point() / 2.0, Geometry::Ellipse(frame.size)) + } + }; + + frame.prepend(pos, Element::Geometry(geometry, fill)); + } + + frames + } +} + +impl From for LayoutNode { + fn from(shape: ShapeNode) -> Self { + Self::new(shape) + } +} diff --git a/src/layout/tree.rs b/src/layout/tree.rs index 38ba6e85c..900eb1a9a 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -50,8 +50,9 @@ impl PageRun { } /// A dynamic layouting node. +#[derive(Clone)] pub struct LayoutNode { - node: Box, + node: Rc, #[cfg(feature = "layout-cache")] hash: u64, } @@ -63,7 +64,7 @@ impl LayoutNode { where T: Layout + 'static, { - Self { node: Box::new(node) } + Self { node: Rc::new(node) } } /// Create a new instance from any node that satisifies the required bounds. @@ -79,7 +80,7 @@ impl LayoutNode { state.finish() }; - Self { node: Box::new(node), hash } + Self { node: Rc::new(node), hash } } } @@ -99,10 +100,16 @@ impl Layout for LayoutNode { ctx.level -= 1; let entry = FramesEntry::new(frames.clone(), ctx.level); - debug_assert!( - entry.check(regions), - "constraints did not match regions they were created for", - ); + + #[cfg(debug_assertions)] + if !entry.check(regions) { + eprintln!("regions: {:#?}", regions); + eprintln!( + "constraints: {:#?}", + frames.iter().map(|c| c.constraints).collect::>() + ); + panic!("constraints did not match regions they were created for"); + } ctx.layouts.insert(self.hash, entry); frames diff --git a/src/library/elements.rs b/src/library/elements.rs index 51f8dc984..5d87d65db 100644 --- a/src/library/elements.rs +++ b/src/library/elements.rs @@ -1,11 +1,8 @@ -use std::f64::consts::SQRT_2; use std::io; -use decorum::N64; - use super::*; use crate::diag::Error; -use crate::layout::{BackgroundNode, BackgroundShape, FixedNode, ImageNode, PadNode}; +use crate::layout::{ImageNode, ShapeKind, ShapeNode}; /// `image`: An image. pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { @@ -33,52 +30,24 @@ pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult { let width = args.named("width")?; let height = args.named("height")?; let fill = args.named("fill")?; - let body = args.eat().unwrap_or_default(); - Ok(rect_impl(width, height, None, fill, body)) + let body = args.eat(); + Ok(shape_impl(ShapeKind::Rect, width, height, fill, body)) } /// `square`: A square with optional content. pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult { let size = args.named::("size")?.map(Linear::from); let width = match size { - Some(size) => Some(size), None => args.named("width")?, + size => size, }; - let height = match width { - Some(_) => None, + let height = match size { None => args.named("height")?, + size => size, }; - let aspect = Some(N64::from(1.0)); let fill = args.named("fill")?; - let body = args.eat().unwrap_or_default(); - Ok(rect_impl(width, height, aspect, fill, body)) -} - -fn rect_impl( - width: Option, - height: Option, - aspect: Option, - fill: Option, - body: Template, -) -> Value { - Value::Template(Template::from_inline(move |style| { - let mut node = LayoutNode::new(FixedNode { - width, - height, - aspect, - child: body.to_stack(style).into(), - }); - - if let Some(fill) = fill { - node = LayoutNode::new(BackgroundNode { - shape: BackgroundShape::Rect, - fill: Paint::Color(fill), - child: node, - }); - } - - node - })) + let body = args.eat(); + Ok(shape_impl(ShapeKind::Square, width, height, fill, body)) } /// `ellipse`: An ellipse with optional content. @@ -86,8 +55,8 @@ pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult { let width = args.named("width")?; let height = args.named("height")?; let fill = args.named("fill")?; - let body = args.eat().unwrap_or_default(); - Ok(ellipse_impl(width, height, None, fill, body)) + let body = args.eat(); + Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body)) } /// `circle`: A circle with optional content. @@ -97,46 +66,39 @@ pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult { None => args.named("width")?, diameter => diameter, }; - let height = match width { + let height = match diameter { None => args.named("height")?, - width => width, + diameter => diameter, }; - let aspect = Some(N64::from(1.0)); let fill = args.named("fill")?; - let body = args.eat().unwrap_or_default(); - Ok(ellipse_impl(width, height, aspect, fill, body)) + let body = args.eat(); + Ok(shape_impl(ShapeKind::Circle, width, height, fill, body)) } -fn ellipse_impl( - width: Option, - height: Option, - aspect: Option, +fn shape_impl( + shape: ShapeKind, + mut width: Option, + mut height: Option, fill: Option, - body: Template, + body: Option