From a6f260ca39f70f82617eca87855789413715f47d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 19 Aug 2021 15:31:29 +0200 Subject: [PATCH] Refactor layouting a bit Notably: - Handle aspect ratio in fixed node - Inline constraint inflation into pad node --- src/eval/mod.rs | 1 - src/eval/template.rs | 2 +- src/geom/size.rs | 15 ++--- src/layout/background.rs | 11 ++-- src/layout/constraints.rs | 36 ++---------- src/layout/fixed.rs | 77 +++++++++++++++++++----- src/layout/frame.rs | 6 +- src/layout/grid.rs | 7 ++- src/layout/image.rs | 7 +-- src/layout/incremental.rs | 101 ++++++++++++++------------------ src/layout/mod.rs | 113 ++++-------------------------------- src/layout/pad.rs | 65 ++++++++++++--------- src/layout/regions.rs | 93 +++++++++++++++++++++++++++++ src/layout/stack.rs | 31 +--------- src/layout/tree.rs | 5 +- src/lib.rs | 36 ++++++++++-- src/library/elements.rs | 36 +++++------- src/library/layout.rs | 15 ++--- src/library/mod.rs | 1 + tests/ref/insert/square.png | Bin 7069 -> 6213 bytes tests/typ/insert/square.typ | 8 ++- 21 files changed, 350 insertions(+), 316 deletions(-) create mode 100644 src/layout/regions.rs diff --git a/src/eval/mod.rs b/src/eval/mod.rs index d49893713..f7a32127c 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -834,7 +834,6 @@ fn walk_item(ctx: &mut EvalContext, label: EcoString, body: Template) { }; StackNode { dirs: Gen::new(state.dirs.main, state.dirs.cross), - aspect: None, children: vec![ StackChild::Any(label.into(), Gen::default()), StackChild::Spacing((state.font.size / 2.0).into()), diff --git a/src/eval/template.rs b/src/eval/template.rs index 595e55547..2293796b7 100644 --- a/src/eval/template.rs +++ b/src/eval/template.rs @@ -425,7 +425,7 @@ impl StackBuilder { children.extend(last.any()); children.push(par); } - StackNode { dirs, aspect: None, children } + StackNode { dirs, children } } } diff --git a/src/geom/size.rs b/src/geom/size.rs index 7967dbdc7..c191a80c3 100644 --- a/src/geom/size.rs +++ b/src/geom/size.rs @@ -30,6 +30,14 @@ impl Size { Self { width: value, height: value } } + /// Limit width and height at that of another size. + pub fn cap(self, limit: Self) -> Self { + Self { + width: self.width.min(limit.width), + height: self.height.min(limit.height), + } + } + /// Whether the other size fits into this one (smaller width and height). pub fn fits(self, other: Self) -> bool { self.width.fits(other.width) && self.height.fits(other.height) @@ -62,13 +70,6 @@ impl Size { SpecAxis::Vertical => Gen::new(self.width, self.height), } } - - /// Find the largest contained size that satisfies the given `aspect` ratio. - pub fn with_aspect(self, aspect: f64) -> Self { - let width = self.width.min(aspect * self.height); - let height = width / aspect; - Size::new(width, height) - } } impl Get for Size { diff --git a/src/layout/background.rs b/src/layout/background.rs index 76ce431b2..793782fd8 100644 --- a/src/layout/background.rs +++ b/src/layout/background.rs @@ -26,9 +26,8 @@ impl Layout for BackgroundNode { regions: &Regions, ) -> Vec>> { let mut frames = self.child.layout(ctx, regions); - for frame in &mut frames { - let mut new = Frame::new(frame.size, frame.baseline); + for Constrained { item: frame, .. } in &mut frames { let (point, geometry) = match self.shape { BackgroundShape::Rect => (Point::zero(), Geometry::Rect(frame.size)), BackgroundShape::Ellipse => { @@ -36,11 +35,15 @@ impl Layout for BackgroundNode { } }; - let prev = std::mem::take(&mut frame.item); + // 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); - *Rc::make_mut(&mut frame.item) = new; } + frames } } diff --git a/src/layout/constraints.rs b/src/layout/constraints.rs index d13433ec4..1a26daeb8 100644 --- a/src/layout/constraints.rs +++ b/src/layout/constraints.rs @@ -1,7 +1,5 @@ use std::ops::Deref; -use crate::util::OptionExt; - use super::*; /// Carries an item that is only valid in certain regions and the constraints @@ -61,36 +59,14 @@ impl Constraints { && base.eq_by(&self.base, |x, y| y.map_or(true, |y| x.approx_eq(y))) } - /// Set the appropriate base constraints for (relative) width and height - /// metrics, respectively. - pub fn set_base_using_linears( - &mut self, - size: Spec>, - regions: &Regions, - ) { + /// Set the appropriate base constraints for linear width and height sizing. + pub fn set_base_if_linear(&mut self, base: Size, sizing: Spec>) { // The full sizes need to be equal if there is a relative component in the sizes. - if size.horizontal.map_or(false, |l| l.is_relative()) { - self.base.horizontal = Some(regions.base.width); + if sizing.horizontal.map_or(false, |l| l.is_relative()) { + self.base.horizontal = Some(base.width); } - if size.vertical.map_or(false, |l| l.is_relative()) { - self.base.vertical = Some(regions.base.height); + if sizing.vertical.map_or(false, |l| l.is_relative()) { + self.base.vertical = Some(base.height); } } - - /// Changes all constraints by adding the `size` to them if they are `Some`. - pub fn inflate(&mut self, size: Size, regions: &Regions) { - for spec in [&mut self.min, &mut self.max] { - if let Some(horizontal) = spec.horizontal.as_mut() { - *horizontal += size.width; - } - if let Some(vertical) = spec.vertical.as_mut() { - *vertical += size.height; - } - } - - self.exact.horizontal.and_set(Some(regions.current.width)); - self.exact.vertical.and_set(Some(regions.current.height)); - self.base.horizontal.and_set(Some(regions.base.width)); - self.base.vertical.and_set(Some(regions.base.height)); - } } diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs index 9fa2af8ac..7f292f14e 100644 --- a/src/layout/fixed.rs +++ b/src/layout/fixed.rs @@ -1,3 +1,5 @@ +use decorum::N64; + use super::*; /// A node that can fix its child's width and height. @@ -8,6 +10,10 @@ pub struct FixedNode { 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, } @@ -16,33 +22,74 @@ impl Layout for FixedNode { fn layout( &self, ctx: &mut LayoutContext, - regions: &Regions, + &Regions { current, base, expand, .. }: &Regions, ) -> Vec>> { - let Regions { current, base, .. } = regions; - let mut constraints = Constraints::new(regions.expand); - constraints.set_base_using_linears(Spec::new(self.width, self.height), ®ions); + // 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)); - let size = Size::new( - self.width.map_or(current.width, |w| w.resolve(base.width)), - self.height.map_or(current.height, |h| h.resolve(base.height)), - ); + // Prepare constraints. + let mut constraints = Constraints::new(expand); + constraints.set_base_if_linear(base, Spec::new(width, height)); - // If one dimension was not specified, the `current` size needs to remain static. - if self.width.is_none() { + // 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.horizontal = Some(current.width); } - if self.height.is_none() { + + if height.is_none() { constraints.exact.vertical = Some(current.height); } - let expand = Spec::new(self.width.is_some(), self.height.is_some()); - let regions = Regions::one(size, expand); + // Resolve the linears based on the current width and height. + let mut size = Size::new( + width.map_or(current.width, |w| w.resolve(base.width)), + height.map_or(current.height, |h| h.resolve(base.height)), + ); + + // If width or height aren't set for an axis, the base should be + // inherited from the parent for that axis. + let base = Size::new( + width.map_or(base.width, |_| size.width), + height.map_or(base.height, |_| size.height), + ); + + // Handle the aspect ratio. + if let Some(aspect) = aspect { + constraints.exact = current.to_spec().map(Some); + constraints.min = Spec::splat(None); + constraints.max = Spec::splat(None); + + let width = size.width.min(aspect * size.height); + size = Size::new(width, width / aspect); + } + + // If width or height are fixed, the child should fill the available + // space along that axis. + let expand = Spec::new(width.is_some(), height.is_some()); + + // Layout the child. + let mut regions = Regions::one(size, base, expand); let mut frames = self.child.layout(ctx, ®ions); - if let Some(frame) = frames.first_mut() { - frame.constraints = constraints; + // 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].size.cap(size); + let width = needed.width.max(aspect * needed.height); + 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. + frames[0].constraints = constraints; + assert_eq!(frames.len(), 1); + frames } } diff --git a/src/layout/frame.rs b/src/layout/frame.rs index 2e3b838e6..0c307dd49 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -19,11 +19,13 @@ pub struct Frame { children: Vec<(Point, Child)>, } -/// A frame can contain multiple children: elements or other frames, complete -/// with their children. +/// A frame can contain two different kinds of children: a leaf element or a +/// nested frame. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] enum Child { + /// A leaf node in the frame tree. Element(Element), + /// An interior node. Frame(Rc), } diff --git a/src/layout/grid.rs b/src/layout/grid.rs index 0a1895134..ed408ab84 100644 --- a/src/layout/grid.rs +++ b/src/layout/grid.rs @@ -263,7 +263,7 @@ impl<'a> GridLayouter<'a> { let mut resolved = Length::zero(); for node in (0 .. self.rows.len()).filter_map(|y| self.cell(x, y)) { let size = Gen::new(available, Length::inf()).to_size(self.main); - let regions = Regions::one(size, Spec::splat(false)); + let regions = Regions::one(size, size, Spec::splat(false)); let frame = node.layout(ctx, ®ions).remove(0); resolved.set_max(frame.size.get(self.cross)); } @@ -405,7 +405,7 @@ impl<'a> GridLayouter<'a> { for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(node) = self.cell(x, y) { let size = Gen::new(rcol, length).to_size(self.main); - let regions = Regions::one(size, Spec::splat(true)); + let regions = Regions::one(size, size, Spec::splat(true)); let frame = node.layout(ctx, ®ions).remove(0); output.push_frame(pos.to_point(self.main), frame.item); } @@ -432,7 +432,8 @@ impl<'a> GridLayouter<'a> { .collect(); // Prepare regions. - let mut regions = Regions::one(self.to_size(first), Spec::splat(true)); + let size = self.to_size(first); + let mut regions = Regions::one(size, size, Spec::splat(true)); regions.backlog = rest.iter().rev().map(|&v| self.to_size(v)).collect(); // Layout the row. diff --git a/src/layout/image.rs b/src/layout/image.rs index 2c20642b8..02a907f5e 100644 --- a/src/layout/image.rs +++ b/src/layout/image.rs @@ -19,11 +19,10 @@ impl Layout for ImageNode { fn layout( &self, ctx: &mut LayoutContext, - regions: &Regions, + &Regions { current, base, expand, .. }: &Regions, ) -> Vec>> { - let Regions { current, base, .. } = regions; - let mut constraints = Constraints::new(regions.expand); - constraints.set_base_using_linears(Spec::new(self.width, self.height), regions); + let mut constraints = Constraints::new(expand); + constraints.set_base_if_linear(base, Spec::new(self.width, self.height)); let width = self.width.map(|w| w.resolve(base.width)); let height = self.height.map(|w| w.resolve(base.height)); diff --git a/src/layout/incremental.rs b/src/layout/incremental.rs index 9a788c91b..0fc668c3e 100644 --- a/src/layout/incremental.rs +++ b/src/layout/incremental.rs @@ -6,7 +6,6 @@ use itertools::Itertools; use super::*; -const CACHE_SIZE: usize = 20; const TEMP_LEN: usize = 5; const TEMP_LAST: usize = TEMP_LEN - 1; @@ -23,22 +22,26 @@ pub struct LayoutCache { /// In how many compilations this cache has been used. age: usize, /// What cache eviction policy should be used. - policy: EvictionStrategy, + policy: EvictionPolicy, + /// The maximum number of entries this cache should have. Can be exceeded if + /// there are more must-keep entries. + max_size: usize, } impl LayoutCache { /// Create a new, empty layout cache. - pub fn new(policy: EvictionStrategy) -> Self { + pub fn new(policy: EvictionPolicy, max_size: usize) -> Self { Self { frames: HashMap::default(), age: 0, policy, + max_size, } } /// Whether the cache is empty. pub fn is_empty(&self) -> bool { - self.len() == 0 + self.frames.values().all(|entry| entry.is_empty()) } /// Amount of items in the cache. @@ -108,38 +111,34 @@ impl LayoutCache { } let last = entry.temperature[TEMP_LAST]; - for i in (1 .. TEMP_LEN).rev() { entry.temperature[i] = entry.temperature[i - 1]; } entry.temperature[0] = 0; entry.temperature[TEMP_LAST] += last; - entry.age += 1; } self.evict(); - self.frames.retain(|_, v| !v.is_empty()); } + /// Evict the cache according to the policy. fn evict(&mut self) { let len = self.len(); - if len <= CACHE_SIZE { + if len <= self.max_size { return; } match self.policy { - EvictionStrategy::LeastRecentlyUsed => { + EvictionPolicy::LeastRecentlyUsed => { // We find the element with the largest cooldown that cannot fit // anymore. let threshold = self - .frames - .values() - .flatten() + .entries() .map(|f| Reverse(f.cooldown())) - .k_smallest(len - CACHE_SIZE) + .k_smallest(len - self.max_size) .last() .unwrap() .0; @@ -148,13 +147,11 @@ impl LayoutCache { entries.retain(|e| e.cooldown() < threshold); } } - EvictionStrategy::LeastFrequentlyUsed => { + EvictionPolicy::LeastFrequentlyUsed => { let threshold = self - .frames - .values() - .flatten() + .entries() .map(|f| N32::from(f.hits() as f32 / f.age() as f32)) - .k_smallest(len - CACHE_SIZE) + .k_smallest(len - self.max_size) .last() .unwrap(); @@ -164,30 +161,23 @@ impl LayoutCache { }); } } - EvictionStrategy::Random => { + EvictionPolicy::Random => { // Fraction of items that should be kept. - let threshold = CACHE_SIZE as f32 / len as f32; + let threshold = self.max_size as f32 / len as f32; for entries in self.frames.values_mut() { entries.retain(|_| rand::random::() > threshold); } } - EvictionStrategy::Patterns => { - let kept = self - .frames - .values() - .flatten() - .filter(|f| f.properties().must_keep()) - .count(); + EvictionPolicy::Patterns => { + let kept = self.entries().filter(|f| f.properties().must_keep()).count(); - let remaining_capacity = CACHE_SIZE - kept.min(CACHE_SIZE); + let remaining_capacity = self.max_size - kept.min(self.max_size); if len - kept <= remaining_capacity { return; } let threshold = self - .frames - .values() - .flatten() + .entries() .filter(|f| !f.properties().must_keep()) .map(|f| N32::from(f.hits() as f32 / f.age() as f32)) .k_smallest((len - kept) - remaining_capacity) @@ -201,7 +191,7 @@ impl LayoutCache { }); } } - EvictionStrategy::None => {} + EvictionPolicy::None => {} } } } @@ -267,6 +257,11 @@ impl FramesEntry { self.temperature[0] != 0 } + /// Get the total amount of hits over the lifetime of this item. + pub fn hits(&self) -> usize { + self.temperature.iter().sum() + } + /// The amount of consecutive cycles in which this item has not been used. pub fn cooldown(&self) -> usize { let mut cycle = 0; @@ -279,11 +274,7 @@ impl FramesEntry { cycle } - /// Get the total amount of hits over the lifetime of this item. - pub fn hits(&self) -> usize { - self.temperature.iter().sum() - } - + /// Properties that describe how this entry's temperature evolved. pub fn properties(&self) -> PatternProperties { let mut all_zeros = true; let mut multi_use = false; @@ -332,15 +323,13 @@ impl FramesEntry { all_zeros = false; } - decreasing = decreasing && !all_same; - PatternProperties { mature: self.age >= TEMP_LEN, hit: self.temperature[0] >= 1, top_level: self.level == 0, all_zeros, multi_use, - decreasing, + decreasing: decreasing && !all_same, sparse, abandoned, } @@ -349,7 +338,7 @@ impl FramesEntry { /// Cache eviction strategies. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum EvictionStrategy { +pub enum EvictionPolicy { /// Evict the least recently used item. LeastRecentlyUsed, /// Evict the least frequently used item. @@ -362,7 +351,7 @@ pub enum EvictionStrategy { None, } -impl Default for EvictionStrategy { +impl Default for EvictionPolicy { fn default() -> Self { Self::Patterns } @@ -415,23 +404,23 @@ impl PatternProperties { mod tests { use super::*; - fn empty_frame() -> Vec>> { + fn empty_frames() -> Vec>> { vec![Constrained { item: Rc::new(Frame::default()), constraints: Constraints::new(Spec::splat(false)), }] } - fn zero_region() -> Regions { - Regions::one(Size::zero(), Spec::splat(false)) + fn zero_regions() -> Regions { + Regions::one(Size::zero(), Size::zero(), Spec::splat(false)) } #[test] - fn test_temperature() { - let mut cache = LayoutCache::new(EvictionStrategy::None); - let zero_region = zero_region(); - cache.policy = EvictionStrategy::None; - cache.insert(0, empty_frame(), 0); + fn test_incremental_temperature() { + let mut cache = LayoutCache::new(EvictionPolicy::None, 20); + let regions = zero_regions(); + cache.policy = EvictionPolicy::None; + cache.insert(0, empty_frames(), 0); let entry = cache.frames.get(&0).unwrap().first().unwrap(); assert_eq!(entry.age(), 1); @@ -439,7 +428,7 @@ mod tests { assert_eq!(entry.used_cycles, 0); assert_eq!(entry.level, 0); - cache.get(0, &zero_region).unwrap(); + cache.get(0, ®ions).unwrap(); let entry = cache.frames.get(&0).unwrap().first().unwrap(); assert_eq!(entry.age(), 1); assert_eq!(entry.temperature, [1, 0, 0, 0, 0]); @@ -450,7 +439,7 @@ mod tests { assert_eq!(entry.temperature, [0, 1, 0, 0, 0]); assert_eq!(entry.used_cycles, 1); - cache.get(0, &zero_region).unwrap(); + cache.get(0, ®ions).unwrap(); for _ in 0 .. 4 { cache.turnaround(); } @@ -462,10 +451,10 @@ mod tests { } #[test] - fn test_properties() { - let mut cache = LayoutCache::new(EvictionStrategy::None); - cache.policy = EvictionStrategy::None; - cache.insert(0, empty_frame(), 1); + fn test_incremental_properties() { + let mut cache = LayoutCache::new(EvictionPolicy::None, 20); + cache.policy = EvictionPolicy::None; + cache.insert(0, empty_frames(), 1); let props = cache.frames.get(&0).unwrap().first().unwrap().properties(); assert_eq!(props.top_level, false); diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 9700004d6..96bd7e7e2 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -10,6 +10,7 @@ mod image; mod incremental; mod pad; mod par; +mod regions; mod shaping; mod stack; mod tree; @@ -24,13 +25,11 @@ pub use grid::*; pub use incremental::*; pub use pad::*; pub use par::*; +pub use regions::*; pub use shaping::*; pub use stack::*; pub use tree::*; -use std::hash::Hash; -#[cfg(feature = "layout-cache")] -use std::hash::Hasher; use std::rc::Rc; use crate::font::FontStore; @@ -45,16 +44,6 @@ pub fn layout(ctx: &mut Context, tree: &LayoutTree) -> Vec> { tree.layout(&mut ctx) } -/// Layout a node. -pub trait Layout { - /// Layout the node into the given regions. - fn layout( - &self, - ctx: &mut LayoutContext, - regions: &Regions, - ) -> Vec>>; -} - /// The context for layouting. pub struct LayoutContext<'a> { /// Stores parsed font faces. @@ -83,94 +72,12 @@ impl<'a> LayoutContext<'a> { } } -/// A sequence of regions to layout into. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Regions { - /// The remaining size of the current region. - pub current: Size, - /// The base size for relative sizing. - pub base: Size, - /// A stack of followup regions. - /// - /// Note that this is a stack and not a queue! The size of the next region is - /// `backlog.last()`. - pub backlog: Vec, - /// The final region that is repeated once the backlog is drained. - pub last: Option, - /// Whether nodes should expand to fill the regions instead of shrinking to - /// fit the content. - /// - /// This property is only handled by nodes that have the ability to control - /// their own size. - pub expand: Spec, -} - -impl Regions { - /// Create a new region sequence with exactly one region. - pub fn one(size: Size, expand: Spec) -> Self { - Self { - current: size, - base: size, - backlog: vec![], - last: None, - expand, - } - } - - /// Create a new sequence of same-size regions that repeats indefinitely. - pub fn repeat(size: Size, expand: Spec) -> Self { - Self { - current: size, - base: size, - backlog: vec![], - last: Some(size), - expand, - } - } - - /// Create new regions where all sizes are mapped with `f`. - pub fn map(&self, mut f: F) -> Self - where - F: FnMut(Size) -> Size, - { - let mut regions = self.clone(); - regions.mutate(|s| *s = f(*s)); - regions - } - - /// Whether `current` is a fully sized (untouched) copy of the last region. - /// - /// If this is true, calling `next()` will have no effect. - pub fn in_full_last(&self) -> bool { - self.backlog.is_empty() && self.last.map_or(true, |size| self.current == size) - } - - /// An iterator that returns pairs of `(current, base)` that are equivalent - /// to what would be produced by calling [`next()`](Self::next) repeatedly - /// until all regions are exhausted. - pub fn iter(&self) -> impl Iterator + '_ { - let first = std::iter::once((self.current, self.base)); - let backlog = self.backlog.iter().rev(); - let last = self.last.iter().cycle(); - first.chain(backlog.chain(last).map(|&s| (s, s))) - } - - /// Advance to the next region if there is any. - pub fn next(&mut self) { - if let Some(size) = self.backlog.pop().or(self.last) { - self.current = size; - self.base = size; - } - } - - /// Mutate all contained sizes in place. - pub fn mutate(&mut self, mut f: F) - where - F: FnMut(&mut Size), - { - f(&mut self.current); - f(&mut self.base); - self.last.as_mut().map(|x| f(x)); - self.backlog.iter_mut().for_each(f); - } +/// Layout a node. +pub trait Layout { + /// Layout the node into the given regions. + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>>; } diff --git a/src/layout/pad.rs b/src/layout/pad.rs index 31571bb3b..51025e3c1 100644 --- a/src/layout/pad.rs +++ b/src/layout/pad.rs @@ -16,51 +16,64 @@ impl Layout for PadNode { ctx: &mut LayoutContext, regions: &Regions, ) -> Vec>> { - let mut regions = regions.clone(); let mut frames = self.child.layout( ctx, ®ions.map(|size| size - self.padding.resolve(size).size()), ); - for frame in &mut frames { - let padded = solve(self.padding, frame.size); + for (Constrained { item: frame, constraints }, (current, base)) in + frames.iter_mut().zip(regions.iter()) + { + fn solve_axis(length: Length, padding: Linear) -> Length { + (length + padding.abs) / (1.0 - padding.rel.get()) + } + + // Solve for the size `padded` that satisfies (approximately): + // `padded - padding.resolve(padded).size() == size` + let padded = Size::new( + solve_axis(frame.size.width, self.padding.left + self.padding.right), + solve_axis(frame.size.height, self.padding.top + self.padding.bottom), + ); + let padding = self.padding.resolve(padded); let origin = Point::new(padding.left, padding.top); - let mut new = Frame::new(padded, frame.baseline + origin.y); - let prev = std::mem::take(&mut frame.item); - new.push_frame(origin, prev); + // Inflate min and max contraints by the padding. + for spec in [&mut constraints.min, &mut constraints.max] { + if let Some(horizontal) = spec.horizontal.as_mut() { + *horizontal += padding.size().width; + } + if let Some(vertical) = spec.vertical.as_mut() { + *vertical += padding.size().height; + } + } - frame.constraints.inflate(padding.size(), ®ions); + // Set exact and base constraints if the child had them. + constraints.exact.horizontal.and_set(Some(current.width)); + constraints.exact.vertical.and_set(Some(current.height)); + constraints.base.horizontal.and_set(Some(base.width)); + constraints.base.vertical.and_set(Some(base.height)); + // Also set base constraints if the padding is relative. if self.padding.left.is_relative() || self.padding.right.is_relative() { - frame.constraints.base.horizontal = Some(regions.base.width); - } - if self.padding.top.is_relative() || self.padding.bottom.is_relative() { - frame.constraints.base.vertical = Some(regions.base.height); + constraints.base.horizontal = Some(base.width); } - regions.next(); - *Rc::make_mut(&mut frame.item) = new; + if self.padding.top.is_relative() || self.padding.bottom.is_relative() { + constraints.base.vertical = Some(base.height); + } + + // Create a new larger frame and place the child's frame inside it. + let empty = Frame::new(padded, frame.baseline + origin.y); + let prev = std::mem::replace(frame, Rc::new(empty)); + let new = Rc::make_mut(frame); + new.push_frame(origin, prev); } frames } } -/// Solve for the size `padded` that satisfies (approximately): -/// `padded - padding.resolve(padded).size() == size` -fn solve(padding: Sides, size: Size) -> Size { - fn solve_axis(length: Length, padding: Linear) -> Length { - (length + padding.abs) / (1.0 - padding.rel.get()) - } - - Size::new( - solve_axis(size.width, padding.left + padding.right), - solve_axis(size.height, padding.top + padding.bottom), - ) -} - impl From for LayoutNode { fn from(pad: PadNode) -> Self { Self::new(pad) diff --git a/src/layout/regions.rs b/src/layout/regions.rs new file mode 100644 index 000000000..daecca459 --- /dev/null +++ b/src/layout/regions.rs @@ -0,0 +1,93 @@ +use crate::geom::{Size, Spec}; + +/// A sequence of regions to layout into. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Regions { + /// The remaining size of the current region. + pub current: Size, + /// The base size for relative sizing. + pub base: Size, + /// A stack of followup regions. + /// + /// Note that this is a stack and not a queue! The size of the next region is + /// `backlog.last()`. + pub backlog: Vec, + /// The final region that is repeated once the backlog is drained. + pub last: Option, + /// Whether nodes should expand to fill the regions instead of shrinking to + /// fit the content. + /// + /// This property is only handled by nodes that have the ability to control + /// their own size. + pub expand: Spec, +} + +impl Regions { + /// Create a new region sequence with exactly one region. + pub fn one(size: Size, base: Size, expand: Spec) -> Self { + Self { + current: size, + base, + backlog: vec![], + last: None, + expand, + } + } + + /// Create a new sequence of same-size regions that repeats indefinitely. + pub fn repeat(size: Size, base: Size, expand: Spec) -> Self { + Self { + current: size, + base, + backlog: vec![], + last: Some(size), + expand, + } + } + + /// Create new regions where all sizes are mapped with `f`. + pub fn map(&self, mut f: F) -> Self + where + F: FnMut(Size) -> Size, + { + let mut regions = self.clone(); + regions.mutate(|s| *s = f(*s)); + regions + } + + /// Whether `current` is a fully sized (untouched) copy of the last region. + /// + /// If this is true, calling `next()` will have no effect. + pub fn in_full_last(&self) -> bool { + self.backlog.is_empty() && self.last.map_or(true, |size| self.current == size) + } + + /// An iterator that returns pairs of `(current, base)` that are equivalent + /// to what would be produced by calling [`next()`](Self::next) repeatedly + /// until all regions are exhausted. + pub fn iter(&self) -> impl Iterator + '_ { + let first = std::iter::once((self.current, self.base)); + let backlog = self.backlog.iter().rev(); + let last = self.last.iter().cycle(); + first.chain(backlog.chain(last).map(|&s| (s, s))) + } + + /// Advance to the next region if there is any. + pub fn next(&mut self) { + if let Some(size) = self.backlog.pop().or(self.last) { + self.current = size; + self.base = size; + } + } + + /// Mutate all contained sizes in place. + pub fn mutate(&mut self, mut f: F) + where + F: FnMut(&mut Size), + { + f(&mut self.current); + f(&mut self.base); + self.last.as_mut().map(|x| f(x)); + self.backlog.iter_mut().for_each(f); + } +} diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 504c64aaf..51d17807d 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -1,5 +1,3 @@ -use decorum::N64; - use super::*; /// A node that stacks its children. @@ -11,10 +9,6 @@ pub struct StackNode { /// The children are stacked along the `main` direction. The `cross` /// direction is required for aligning the children. pub dirs: Gen, - /// The fixed aspect ratio between width and height, if any. - /// - /// The resulting frames will satisfy `width = aspect * height`. - pub aspect: Option, /// The nodes to be stacked. pub children: Vec, } @@ -83,10 +77,6 @@ impl<'a> StackLayouter<'a> { // Disable expansion on the main axis for children. regions.expand.set(main, false); - if let Some(aspect) = stack.aspect { - regions.current = regions.current.with_aspect(aspect.into_inner()); - } - Self { stack, main, @@ -161,6 +151,7 @@ impl<'a> StackLayouter<'a> { .max .get_mut(self.main) .set_min(self.used.main + size.main); + self.finish_region(); } @@ -184,7 +175,7 @@ impl<'a> StackLayouter<'a> { // Determine the stack's size dependening on whether the region is // fixed. - let mut size = Size::new( + let size = Size::new( if expand.horizontal { self.constraints.exact.horizontal = Some(self.full.width); self.full.width @@ -201,20 +192,6 @@ impl<'a> StackLayouter<'a> { }, ); - // Make sure the stack's size satisfies the aspect ratio. - if let Some(aspect) = self.stack.aspect { - self.constraints.exact = self.full.to_spec().map(Some); - self.constraints.min = Spec::splat(None); - self.constraints.max = Spec::splat(None); - let width = size - .width - .max(aspect.into_inner() * size.height) - .min(self.full.width) - .min(aspect.into_inner() * self.full.height); - - size = Size::new(width, width / aspect.into_inner()); - } - if self.overflowing { self.constraints.min.vertical = None; self.constraints.max.vertical = None; @@ -259,10 +236,6 @@ impl<'a> StackLayouter<'a> { } self.regions.next(); - if let Some(aspect) = self.stack.aspect { - self.regions.current = self.regions.current.with_aspect(aspect.into_inner()); - } - self.full = self.regions.current; self.used = Gen::zero(); self.ruler = Align::Start; diff --git a/src/layout/tree.rs b/src/layout/tree.rs index 4b21e05ca..1899a4d2d 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -3,6 +3,9 @@ use super::*; use std::any::Any; use std::fmt::{self, Debug, Formatter}; +#[cfg(feature = "layout-cache")] +use std::hash::{Hash, Hasher}; + #[cfg(feature = "layout-cache")] use fxhash::FxHasher64; @@ -37,7 +40,7 @@ impl PageRun { // that axis. let Size { width, height } = self.size; let expand = Spec::new(width.is_finite(), height.is_finite()); - let regions = Regions::repeat(self.size, expand); + let regions = Regions::repeat(self.size, self.size, expand); self.child.layout(ctx, ®ions).into_iter().map(|c| c.item).collect() } } diff --git a/src/lib.rs b/src/lib.rs index d646cf6a0..b2d48a2b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ use crate::eval::{Module, Scope, State}; use crate::font::FontStore; use crate::image::ImageStore; #[cfg(feature = "layout-cache")] -use crate::layout::{EvictionStrategy, LayoutCache}; +use crate::layout::{EvictionPolicy, LayoutCache}; use crate::layout::{Frame, LayoutTree}; use crate::loading::Loader; use crate::source::{SourceId, SourceStore}; @@ -137,12 +137,13 @@ impl Context { /// A builder for a [`Context`]. /// /// This struct is created by [`Context::builder`]. -#[derive(Default)] pub struct ContextBuilder { std: Option, state: Option, #[cfg(feature = "layout-cache")] - policy: Option, + policy: EvictionPolicy, + #[cfg(feature = "layout-cache")] + max_size: usize, } impl ContextBuilder { @@ -161,8 +162,18 @@ impl ContextBuilder { /// The policy for eviction of the layout cache. #[cfg(feature = "layout-cache")] - pub fn policy(mut self, policy: EvictionStrategy) -> Self { - self.policy = Some(policy); + pub fn cache_policy(mut self, policy: EvictionPolicy) -> Self { + self.policy = policy; + self + } + + /// The maximum number of entries the layout cache should have. + /// + /// Note that this can be exceeded if more entries are categorized as [must + /// keep][crate::layout::PatternProperties::must_keep]. + #[cfg(feature = "layout-cache")] + pub fn cache_max_size(mut self, max_size: usize) -> Self { + self.max_size = max_size; self } @@ -175,9 +186,22 @@ impl ContextBuilder { images: ImageStore::new(Rc::clone(&loader)), loader, #[cfg(feature = "layout-cache")] - layouts: LayoutCache::new(self.policy.unwrap_or_default()), + layouts: LayoutCache::new(self.policy, self.max_size), std: self.std.unwrap_or(library::new()), state: self.state.unwrap_or_default(), } } } + +impl Default for ContextBuilder { + fn default() -> Self { + Self { + std: None, + state: None, + #[cfg(feature = "layout-cache")] + policy: EvictionPolicy::default(), + #[cfg(feature = "layout-cache")] + max_size: 2000, + } + } +} diff --git a/src/library/elements.rs b/src/library/elements.rs index f90363bb7..6e71626a3 100644 --- a/src/library/elements.rs +++ b/src/library/elements.rs @@ -64,18 +64,19 @@ fn rect_impl( body: Template, ) -> Value { Value::Template(Template::from_inline(move |state| { - let mut stack = body.to_stack(state); - stack.aspect = aspect; - - let mut node = FixedNode { width, height, child: stack.into() }.into(); + let mut node = LayoutNode::new(FixedNode { + width, + height, + aspect, + child: body.to_stack(state).into(), + }); if let Some(fill) = fill { - node = BackgroundNode { + node = LayoutNode::new(BackgroundNode { shape: BackgroundShape::Rect, fill: Paint::Color(fill), child: node, - } - .into(); + }); } node @@ -120,27 +121,22 @@ fn ellipse_impl( // perfectly into the ellipse. const PAD: f64 = 0.5 - SQRT_2 / 4.0; - let mut stack = body.to_stack(state); - stack.aspect = aspect; - - let mut node = FixedNode { + let mut node = LayoutNode::new(FixedNode { width, height, - child: PadNode { + aspect, + child: LayoutNode::new(PadNode { padding: Sides::splat(Relative::new(PAD).into()), - child: stack.into(), - } - .into(), - } - .into(); + child: body.to_stack(state).into(), + }), + }); if let Some(fill) = fill { - node = BackgroundNode { + node = LayoutNode::new(BackgroundNode { shape: BackgroundShape::Ellipse, fill: Paint::Color(fill), child: node, - } - .into(); + }); } node diff --git a/src/library/layout.rs b/src/library/layout.rs index b1510cb6d..91e2e7f3d 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -145,8 +145,12 @@ pub fn boxed(_: &mut EvalContext, args: &mut Arguments) -> TypResult { let height = args.named("height")?; let body: Template = args.eat().unwrap_or_default(); Ok(Value::Template(Template::from_inline(move |state| { - let child = body.to_stack(state).into(); - FixedNode { width, height, child } + FixedNode { + width, + height, + aspect: None, + child: body.to_stack(state).into(), + } }))) } @@ -190,10 +194,7 @@ pub fn stack(_: &mut EvalContext, args: &mut Arguments) -> TypResult { Ok(Value::Template(Template::from_block(move |state| { let children = children .iter() - .map(|child| { - let child = child.to_stack(state).into(); - StackChild::Any(child, state.aligns) - }) + .map(|child| StackChild::Any(child.to_stack(state).into(), state.aligns)) .collect(); let mut dirs = Gen::new(None, dir).unwrap_or(state.dirs); @@ -204,7 +205,7 @@ pub fn stack(_: &mut EvalContext, args: &mut Arguments) -> TypResult { dirs.cross = state.dirs.main; } - StackNode { dirs, aspect: None, children } + StackNode { dirs, children } }))) } diff --git a/src/library/mod.rs b/src/library/mod.rs index dd1574f31..44f1f01f1 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -21,6 +21,7 @@ use crate::diag::TypResult; use crate::eval::{Arguments, EvalContext, Scope, Str, Template, Value}; use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::geom::*; +use crate::layout::LayoutNode; use crate::syntax::Spanned; /// Construct a scope containing all standard library definitions. diff --git a/tests/ref/insert/square.png b/tests/ref/insert/square.png index c84e6f0e583c56a50de192301d506d90ea4a23db..9ecadeeba9ada33088f237bc1e46be9cca21f246 100644 GIT binary patch literal 6213 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

dFt12J;1YZWaZCLW_Ux!&7Qnx**_GLQly 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