From fb0cd3df6e1e1077c6f19c319726c9aa9678325b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 26 Oct 2021 14:51:48 +0200 Subject: [PATCH] Fr in stack and par --- src/eval/template.rs | 24 +-- src/eval/walk.rs | 8 +- src/geom/size.rs | 8 - src/layout/grid.rs | 43 +++-- src/layout/mod.rs | 9 + src/layout/par.rs | 119 ++++++------- src/layout/shape.rs | 3 +- src/layout/stack.rs | 183 ++++++++++++-------- src/library/layout.rs | 21 ++- src/library/mod.rs | 9 + tests/ref/layout/spacing.png | Bin 1744 -> 1827 bytes tests/ref/layout/{stack.png => stack-1.png} | Bin tests/ref/layout/stack-2.png | Bin 0 -> 3298 bytes tests/typ/layout/spacing.typ | 3 + tests/typ/layout/{stack.typ => stack-1.typ} | 0 tests/typ/layout/stack-2.typ | 24 +++ 16 files changed, 257 insertions(+), 197 deletions(-) rename tests/ref/layout/{stack.png => stack-1.png} (100%) create mode 100644 tests/ref/layout/stack-2.png rename tests/typ/layout/{stack.typ => stack-1.typ} (100%) create mode 100644 tests/typ/layout/stack-2.typ diff --git a/src/eval/template.rs b/src/eval/template.rs index fe3d0ccaf..b1559b8fa 100644 --- a/src/eval/template.rs +++ b/src/eval/template.rs @@ -8,8 +8,8 @@ use super::Str; use crate::diag::StrResult; use crate::geom::{Align, Dir, GenAxis, Length, Linear, Sides, Size}; use crate::layout::{ - BlockNode, Decoration, InlineNode, PadNode, PageNode, ParChild, ParNode, StackChild, - StackNode, + BlockNode, Decoration, InlineNode, PadNode, PageNode, ParChild, ParNode, Spacing, + StackChild, StackNode, }; use crate::style::Style; use crate::util::EcoString; @@ -32,7 +32,7 @@ enum TemplateNode { /// Plain text. Text(EcoString), /// Spacing. - Spacing(GenAxis, Linear), + Spacing(GenAxis, Spacing), /// A decorated template. Decorated(Decoration, Template), /// An inline node builder. @@ -107,7 +107,7 @@ impl Template { } /// Add spacing along an axis. - pub fn spacing(&mut self, axis: GenAxis, spacing: Linear) { + pub fn spacing(&mut self, axis: GenAxis, spacing: Spacing) { self.make_mut().push(TemplateNode::Spacing(axis, spacing)); } @@ -308,7 +308,8 @@ impl Builder { fn parbreak(&mut self) { let amount = self.style.par_spacing(); self.stack.finish_par(&self.style); - self.stack.push_soft(StackChild::Spacing(amount.into())); + self.stack + .push_soft(StackChild::Spacing(Spacing::Linear(amount.into()))); } /// Apply a forced page break. @@ -328,25 +329,26 @@ impl Builder { /// Push an inline node into the active paragraph. fn inline(&mut self, node: impl Into) { let align = self.style.aligns.inline; - self.stack.par.push(ParChild::Any(node.into(), align)); + self.stack.par.push(ParChild::Node(node.into(), align)); } /// Push a block node into the active stack, finishing the active paragraph. fn block(&mut self, node: impl Into) { self.parbreak(); - self.stack.push(StackChild::Any(node.into(), self.style.aligns.block)); + self.stack + .push(StackChild::Node(node.into(), self.style.aligns.block)); self.parbreak(); } /// Push spacing into the active paragraph or stack depending on the `axis`. - fn spacing(&mut self, axis: GenAxis, amount: Linear) { + fn spacing(&mut self, axis: GenAxis, spacing: Spacing) { match axis { GenAxis::Block => { self.stack.finish_par(&self.style); - self.stack.push_hard(StackChild::Spacing(amount)); + self.stack.push_hard(StackChild::Spacing(spacing)); } GenAxis::Inline => { - self.stack.par.push_hard(ParChild::Spacing(amount)); + self.stack.par.push_hard(ParChild::Spacing(spacing)); } } } @@ -500,7 +502,7 @@ impl ParBuilder { fn build(self) -> Option { let Self { align, dir, leading, children, .. } = self; (!children.is_empty()) - .then(|| StackChild::Any(ParNode { dir, leading, children }.into(), align)) + .then(|| StackChild::Node(ParNode { dir, leading, children }.into(), align)) } } diff --git a/src/eval/walk.rs b/src/eval/walk.rs index 6ac301507..9dd7cd2e9 100644 --- a/src/eval/walk.rs +++ b/src/eval/walk.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use super::{Eval, EvalContext, Str, Template, Value}; use crate::diag::TypResult; use crate::geom::Align; -use crate::layout::{ParChild, ParNode, StackChild, StackNode}; +use crate::layout::{ParChild, ParNode, Spacing, StackChild, StackNode}; use crate::syntax::*; use crate::util::BoolExt; @@ -118,9 +118,9 @@ fn walk_item(ctx: &mut EvalContext, label: Str, body: Template) { StackNode { dir: style.dir, children: vec![ - StackChild::Any(label.into(), Align::Start), - StackChild::Spacing((style.text.size / 2.0).into()), - StackChild::Any(body.to_stack(&style).into(), Align::Start), + StackChild::Node(label.into(), Align::Start), + StackChild::Spacing(Spacing::Linear((style.text.size / 2.0).into())), + StackChild::Node(body.to_stack(&style).into(), Align::Start), ], } }); diff --git a/src/geom/size.rs b/src/geom/size.rs index 506e1c8fa..2143c46bb 100644 --- a/src/geom/size.rs +++ b/src/geom/size.rs @@ -25,14 +25,6 @@ impl Size { Self { w: v, h: v } } - /// Limit width and height at that of another size. - pub fn cap(self, limit: Self) -> Self { - Self { - w: self.w.min(limit.w), - h: self.h.min(limit.h), - } - } - /// Whether the other size fits into this one (smaller width and height). pub fn fits(self, other: Self) -> bool { self.w.fits(other.w) && self.h.fits(other.h) diff --git a/src/layout/grid.rs b/src/layout/grid.rs index 22581696e..7220d7c2c 100644 --- a/src/layout/grid.rs +++ b/src/layout/grid.rs @@ -17,7 +17,7 @@ pub struct GridNode { pub enum TrackSizing { /// Fit the cell to its contents. Auto, - /// A length stated in absolute values and fractions of the parent's size. + /// A length stated in absolute values and/or relative to the parent's size. Linear(Linear), /// A length that is the fraction of the remaining free space in the parent. Fractional(Fractional), @@ -124,26 +124,23 @@ impl<'a> GridLayouter<'a> { cols.pop(); rows.pop(); - let full = regions.current.h; - let rcols = vec![Length::zero(); cols.len()]; - // We use the regions only for auto row measurement and constraints. let expand = regions.expand; regions.expand = Spec::new(true, false); Self { - cols, - rows, children: &grid.children, cts: Constraints::new(expand), - regions, + full: regions.current.h, expand, - rcols, + rcols: vec![Length::zero(); cols.len()], lrows: vec![], - full, used: Size::zero(), fr: Fractional::zero(), finished: vec![], + cols, + rows, + regions, } } @@ -313,9 +310,9 @@ impl<'a> GridLayouter<'a> { TrackSizing::Auto => self.layout_auto_row(ctx, y), TrackSizing::Linear(v) => self.layout_linear_row(ctx, v, y), TrackSizing::Fractional(v) => { - self.fr += v; self.cts.exact.y = Some(self.full); self.lrows.push(Row::Fr(v, y)); + self.fr += v; } } } @@ -498,22 +495,22 @@ impl<'a> GridLayouter<'a> { /// Finish rows for one region. fn finish_region(&mut self, ctx: &mut LayoutContext) { - // Determine the height of the region's frame. - let height = if self.fr.is_zero() || self.full.is_infinite() { - self.used.h - } else { - self.full - }; - - self.cts.min.y = Some(height); - - // The frame for the region. - let mut output = Frame::new(Size::new(self.used.w, height), height); - let mut pos = Point::zero(); - // Determine the size that remains for fractional rows. let remaining = self.full - self.used.h; + // Determine the size of the grid in this region, expanding fully if + // there are fr rows. + let mut size = self.used; + if !self.fr.is_zero() && self.full.is_finite() { + size.h = self.full; + } + + self.cts.min.y = Some(size.h); + + // The frame for the region. + let mut output = Frame::new(size, size.h); + let mut pos = Point::zero(); + // Place finished rows and layout fractional rows. for row in std::mem::take(&mut self.lrows) { let frame = match row { diff --git a/src/layout/mod.rs b/src/layout/mod.rs index d03137789..651daa21a 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -276,3 +276,12 @@ fn hash_node(node: &(impl Hash + 'static)) -> u64 { node.hash(&mut state); state.finish() } + +/// Kinds of spacing. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Spacing { + /// A length stated in absolute values and/or relative to the parent's size. + Linear(Linear), + /// A length that is the fraction of the remaining free space in the parent. + Fractional(Fractional), +} diff --git a/src/layout/par.rs b/src/layout/par.rs index 4a1e37434..64265b63e 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -19,7 +19,7 @@ pub struct ParNode { pub dir: Dir, /// The spacing to insert between each line. pub leading: Length, - /// The nodes to be arranged in a paragraph. + /// The children to be arranged in a paragraph. pub children: Vec, } @@ -27,11 +27,11 @@ pub struct ParNode { #[cfg_attr(feature = "layout-cache", derive(Hash))] pub enum ParChild { /// Spacing between other nodes. - Spacing(Linear), + Spacing(Spacing), /// A run of text and how to align it in its line. Text(EcoString, Align, Rc), /// Any child node and how to align it in its line. - Any(InlineNode, Align), + Node(InlineNode, Align), /// A decoration that applies until a matching `Undecorate`. Decorate(Decoration), /// The end of a decoration. @@ -87,7 +87,7 @@ impl ParNode { self.children.iter().map(|child| match child { ParChild::Spacing(_) => " ", ParChild::Text(ref piece, ..) => piece, - ParChild::Any(..) => "\u{FFFC}", + ParChild::Node(..) => "\u{FFFC}", ParChild::Decorate(_) | ParChild::Undecorate => "", }) } @@ -104,7 +104,7 @@ impl Debug for ParChild { match self { Self::Spacing(v) => write!(f, "Spacing({:?})", v), Self::Text(text, ..) => write!(f, "Text({:?})", text), - Self::Any(node, ..) => node.fmt(f), + Self::Node(node, ..) => node.fmt(f), Self::Decorate(deco) => write!(f, "Decorate({:?})", deco), Self::Undecorate => write!(f, "Undecorate"), } @@ -120,7 +120,7 @@ struct ParLayouter<'a> { leading: Length, /// Bidirectional text embedding levels for the paragraph. bidi: BidiInfo<'a>, - /// Layouted children and separated text runs. + /// Spacing, separated text runs and layouted nodes. items: Vec>, /// The ranges of the items in `bidi.text`. ranges: Vec, @@ -130,8 +130,10 @@ struct ParLayouter<'a> { /// A prepared item in a paragraph layout. enum ParItem<'a> { - /// Spacing between other items. - Spacing(Length), + /// Absolute spacing between other items. + Absolute(Length), + /// Fractional spacing between other items. + Fractional(Fractional), /// A shaped text run with consistent direction. Text(ShapedText<'a>, Align), /// A layouted child node. @@ -153,27 +155,35 @@ impl<'a> ParLayouter<'a> { // Layout the children and collect them into items. for (range, child) in par.ranges().zip(&par.children) { - match child { - ParChild::Spacing(amount) => { - let resolved = amount.resolve(regions.current.w); - items.push(ParItem::Spacing(resolved)); + match *child { + ParChild::Spacing(Spacing::Linear(v)) => { + let resolved = v.resolve(regions.current.w); + items.push(ParItem::Absolute(resolved)); ranges.push(range); } - ParChild::Text(_, align, style) => { + ParChild::Spacing(Spacing::Fractional(v)) => { + items.push(ParItem::Fractional(v)); + ranges.push(range); + } + ParChild::Text(_, align, ref style) => { // TODO: Also split by language and script. - for (subrange, dir) in split_runs(&bidi, range) { + let mut cursor = range.start; + for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) { + let start = cursor; + cursor += group.len(); + let subrange = start .. cursor; let text = &bidi.text[subrange.clone()]; - let shaped = shape(ctx, text, style, dir); - items.push(ParItem::Text(shaped, *align)); + let shaped = shape(ctx, text, style, level.dir()); + items.push(ParItem::Text(shaped, align)); ranges.push(subrange); } } - ParChild::Any(node, align) => { + ParChild::Node(ref node, align) => { let frame = node.layout(ctx, regions.current.w, regions.base); - items.push(ParItem::Frame(frame, *align)); + items.push(ParItem::Frame(frame, align)); ranges.push(range); } - ParChild::Decorate(deco) => { + ParChild::Decorate(ref deco) => { starts.push((range.start, deco)); } ParChild::Undecorate => { @@ -298,41 +308,6 @@ impl<'a> ParLayouter<'a> { } } -/// Split a range of text into runs of consistent direction. -fn split_runs<'a>( - bidi: &'a BidiInfo, - range: Range, -) -> impl Iterator + 'a { - let mut cursor = range.start; - bidi.levels[range] - .group_by_key(|&level| level) - .map(move |(level, group)| { - let start = cursor; - cursor += group.len(); - (start .. cursor, level.dir()) - }) -} - -impl ParItem<'_> { - /// The size of the item. - pub fn size(&self) -> Size { - match self { - Self::Spacing(amount) => Size::new(*amount, Length::zero()), - Self::Text(shaped, ..) => shaped.size, - Self::Frame(frame, ..) => frame.size, - } - } - - /// The baseline of the item. - pub fn baseline(&self) -> Length { - match self { - Self::Spacing(_) => Length::zero(), - Self::Text(shaped, ..) => shaped.baseline, - Self::Frame(frame, ..) => frame.baseline, - } - } -} - /// A lightweight representation of a line that spans a specific range in a /// paragraph's text. This type enables you to cheaply measure the size of a /// line in a range before comitting to building the line's frame. @@ -356,6 +331,8 @@ struct LineLayout<'a> { size: Size, /// The baseline of the line. baseline: Length, + /// The sum of fractional ratios in the line. + fr: Fractional, } impl<'a> LineLayout<'a> { @@ -422,14 +399,20 @@ impl<'a> LineLayout<'a> { let mut width = Length::zero(); let mut top = Length::zero(); let mut bottom = Length::zero(); + let mut fr = Fractional::zero(); // Measure the size of the line. for item in first.iter().chain(items).chain(&last) { - let size = item.size(); - let baseline = item.baseline(); - width += size.w; - top.set_max(baseline); - bottom.set_max(size.h - baseline); + match *item { + ParItem::Absolute(v) => width += v, + ParItem::Fractional(v) => fr += v, + ParItem::Text(ShapedText { size, baseline, .. }, _) + | ParItem::Frame(Frame { size, baseline, .. }, _) => { + width += size.w; + top.set_max(baseline); + bottom.set_max(size.h - baseline); + } + } } Self { @@ -441,13 +424,14 @@ impl<'a> LineLayout<'a> { ranges, size: Size::new(width, top + bottom), baseline: top, + fr, } } /// Build the line's frame. fn build(&self, ctx: &LayoutContext, width: Length) -> Frame { let size = Size::new(self.size.w.max(width), self.size.h); - let free = size.w - self.size.w; + let remaining = size.w - self.size.w; let mut output = Frame::new(size, self.baseline); let mut offset = Length::zero(); @@ -464,7 +448,7 @@ impl<'a> LineLayout<'a> { // FIXME: Ruler alignment for RTL. ruler = ruler.max(align); - let x = ruler.resolve(self.par.dir, offset .. free + offset); + let x = ruler.resolve(self.par.dir, offset .. remaining + offset); let y = self.baseline - frame.baseline; offset += frame.size.w; @@ -473,7 +457,13 @@ impl<'a> LineLayout<'a> { }; match *item { - ParItem::Spacing(amount) => offset += amount, + ParItem::Absolute(v) => offset += v, + ParItem::Fractional(v) => { + let ratio = v / self.fr; + if remaining.is_finite() && ratio.is_finite() { + offset += ratio * remaining; + } + } ParItem::Text(ref shaped, align) => position(shaped.build(), align), ParItem::Frame(ref frame, align) => position(frame.clone(), align), } @@ -539,6 +529,7 @@ struct LineStack<'a> { finished: Vec>>, cts: Constraints, overflowing: bool, + fractional: bool, } impl<'a> LineStack<'a> { @@ -553,6 +544,7 @@ impl<'a> LineStack<'a> { lines: vec![], finished: vec![], overflowing: false, + fractional: false, } } @@ -566,12 +558,13 @@ impl<'a> LineStack<'a> { self.size.h += self.leading; } + self.fractional |= !line.fr.is_zero(); self.lines.push(line); } /// Finish the frame for one region. fn finish_region(&mut self, ctx: &LayoutContext) { - if self.regions.expand.x { + if self.regions.expand.x || self.fractional { self.size.w = self.regions.current.w; self.cts.exact.x = Some(self.regions.current.w); } diff --git a/src/layout/shape.rs b/src/layout/shape.rs index 13d5418f4..2e66a0ddf 100644 --- a/src/layout/shape.rs +++ b/src/layout/shape.rs @@ -55,8 +55,7 @@ impl InlineLevel for ShapeNode { // The "pod" is the region into which the child will be layouted. let mut pod = { - let size = - Size::new(width.unwrap_or(space), height.unwrap_or(Length::inf())); + let size = Size::new(width.unwrap_or(space), height.unwrap_or(base.h)); let base = Size::new( if width.is_some() { size.w } else { base.w }, diff --git a/src/layout/stack.rs b/src/layout/stack.rs index bbaf022b4..dbd9ddb08 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -8,7 +8,7 @@ use super::*; pub struct StackNode { /// The stacking direction. pub dir: Dir, - /// The nodes to be stacked. + /// The children to be stacked. pub children: Vec, } @@ -16,9 +16,9 @@ pub struct StackNode { #[cfg_attr(feature = "layout-cache", derive(Hash))] pub enum StackChild { /// Spacing between other nodes. - Spacing(Linear), + Spacing(Spacing), /// Any block node and how to align it in the stack. - Any(BlockNode, Align), + Node(BlockNode, Align), } impl BlockLevel for StackNode { @@ -41,7 +41,7 @@ impl Debug for StackChild { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Spacing(v) => write!(f, "Spacing({:?})", v), - Self::Any(node, _) => node.fmt(f), + Self::Node(node, _) => node.fmt(f), } } } @@ -61,34 +61,41 @@ struct StackLayouter<'a> { full: Size, /// The generic size used by the frames for the current region. used: Gen, - /// The alignment ruler for the current region. - ruler: Align, - /// Offset, alignment and frame for all children that fit into the current - /// region. The exact positions are not known yet. - frames: Vec<(Length, Align, Rc)>, + /// The sum of fractional ratios in the current region. + fr: Fractional, + /// Spacing and layouted nodes. + items: Vec, /// Finished frames for previous regions. finished: Vec>>, } +/// A prepared item in a stack layout. +enum StackItem { + /// Absolute spacing between other items. + Absolute(Length), + /// Fractional spacing between other items. + Fractional(Fractional), + /// A layouted child node. + Frame(Rc, Align), +} + impl<'a> StackLayouter<'a> { /// Create a new stack layouter. fn new(stack: &'a StackNode, mut regions: Regions) -> Self { - let axis = stack.dir.axis(); - let full = regions.current; - let expand = regions.expand; - // Disable expansion along the block axis for children. + let axis = stack.dir.axis(); + let expand = regions.expand; regions.expand.set(axis, false); Self { stack, axis, expand, + full: regions.current, regions, - full, used: Gen::zero(), - ruler: Align::Start, - frames: vec![], + fr: Fractional::zero(), + items: vec![], finished: vec![], } } @@ -97,16 +104,15 @@ impl<'a> StackLayouter<'a> { fn layout(mut self, ctx: &mut LayoutContext) -> Vec>> { for child in &self.stack.children { match *child { - StackChild::Spacing(amount) => self.space(amount), - StackChild::Any(ref node, align) => { - let frames = node.layout(ctx, &self.regions); - let len = frames.len(); - for (i, frame) in frames.into_iter().enumerate() { - self.push_frame(frame.item, align); - if i + 1 < len { - self.finish_region(); - } - } + StackChild::Spacing(Spacing::Linear(v)) => { + self.layout_absolute(v); + } + StackChild::Spacing(Spacing::Fractional(v)) => { + self.items.push(StackItem::Fractional(v)); + self.fr += v; + } + StackChild::Node(ref node, align) => { + self.layout_node(ctx, node, align); } } } @@ -115,75 +121,102 @@ impl<'a> StackLayouter<'a> { self.finished } - /// Add block-axis spacing into the current region. - fn space(&mut self, amount: Linear) { - // Resolve the linear. - let full = self.full.get(self.axis); - let resolved = amount.resolve(full); - - // Cap the spacing to the remaining available space. This action does - // not directly affect the constraints because of the cap. + /// Layout absolute spacing. + fn layout_absolute(&mut self, amount: Linear) { + // Resolve the linear, limiting it to the remaining available space. let remaining = self.regions.current.get_mut(self.axis); - let capped = resolved.min(*remaining); - - // Grow our size and shrink the available space in the region. - self.used.block += capped; - *remaining -= capped; + let resolved = amount.resolve(self.full.get(self.axis)); + let limited = resolved.min(*remaining); + *remaining -= limited; + self.used.block += limited; + self.items.push(StackItem::Absolute(resolved)); } - /// Push a frame into the current region. - fn push_frame(&mut self, frame: Rc, align: Align) { - // Grow our size. - let offset = self.used.block; - let size = frame.size.to_gen(self.axis); - self.used.block += size.block; - self.used.inline.set_max(size.inline); - self.ruler = self.ruler.max(align); + /// Layout a block node. + fn layout_node(&mut self, ctx: &mut LayoutContext, node: &BlockNode, align: Align) { + let frames = node.layout(ctx, &self.regions); + let len = frames.len(); + for (i, frame) in frames.into_iter().enumerate() { + // Grow our size. + let size = frame.item.size.to_gen(self.axis); + self.used.block += size.block; + self.used.inline.set_max(size.inline); - // Remember the frame and shrink available space in the region for the - // following children. - self.frames.push((offset, self.ruler, frame)); - *self.regions.current.get_mut(self.axis) -= size.block; + // Remember the frame and shrink available space in the region for the + // following children. + self.items.push(StackItem::Frame(frame.item, align)); + *self.regions.current.get_mut(self.axis) -= size.block; + + if i + 1 < len { + self.finish_region(); + } + } } /// Finish the frame for one region. fn finish_region(&mut self) { - // Determine the stack's size dependening on whether the region expands. + // Determine the size that remains for fractional spacing. + let remaining = self.full.get(self.axis) - self.used.block; + + // Determine the size of the stack in this region dependening on whether + // the region expands. let used = self.used.to_size(self.axis); - let size = Size::new( + let mut size = Size::new( if self.expand.x { self.full.w } else { used.w }, if self.expand.y { self.full.h } else { used.h }, ); + // Expand fully if there are fr spacings. + let full = self.full.get(self.axis); + if !self.fr.is_zero() && full.is_finite() { + size.set(self.axis, full); + } + let mut output = Frame::new(size, size.h); + let mut before = Length::zero(); + let mut ruler = Align::Start; let mut first = true; // Place all frames. - for (offset, align, frame) in self.frames.drain(..) { - let stack_size = size.to_gen(self.axis); - let child_size = frame.size.to_gen(self.axis); + for item in self.items.drain(..) { + match item { + StackItem::Absolute(v) => before += v, + StackItem::Fractional(v) => { + let ratio = v / self.fr; + if remaining.is_finite() && ratio.is_finite() { + before += ratio * remaining; + } + } + StackItem::Frame(frame, align) => { + ruler = ruler.max(align); - // Align along the block axis. - let block = align.resolve( - self.stack.dir, - if self.stack.dir.is_positive() { - offset .. stack_size.block - self.used.block + offset - } else { - let offset_with_self = offset + child_size.block; - self.used.block - offset_with_self - .. stack_size.block - offset_with_self - }, - ); + let parent = size.to_gen(self.axis); + let child = frame.size.to_gen(self.axis); - let pos = Gen::new(Length::zero(), block).to_point(self.axis); + // Align along the block axis. + let block = ruler.resolve( + self.stack.dir, + if self.stack.dir.is_positive() { + let after = self.used.block - before; + before .. parent.block - after + } else { + let before_with_self = before + child.block; + let after = self.used.block - before_with_self; + after .. parent.block - before_with_self + }, + ); - // The baseline of the stack is that of the first frame. - if first { - output.baseline = pos.y + frame.baseline; - first = false; + let pos = Gen::new(Length::zero(), block).to_point(self.axis); + if first { + // The baseline of the stack is that of the first frame. + output.baseline = pos.y + frame.baseline; + first = false; + } + + output.push_frame(pos, frame); + before += child.block; + } } - - output.push_frame(pos, frame); } // Generate tight constraints for now. @@ -194,7 +227,7 @@ impl<'a> StackLayouter<'a> { self.regions.next(); self.full = self.regions.current; self.used = Gen::zero(); - self.ruler = Align::Start; + self.fr = Fractional::zero(); self.finished.push(output.constrain(cts)); } } diff --git a/src/library/layout.rs b/src/library/layout.rs index 4b416156f..4ef0926fd 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -130,17 +130,15 @@ pub fn align(ctx: &mut EvalContext, args: &mut Args) -> TypResult { /// `h`: Horizontal spacing. pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult { - let spacing = args.expect("spacing")?; let mut template = Template::new(); - template.spacing(GenAxis::Inline, spacing); + template.spacing(GenAxis::Inline, args.expect("spacing")?); Ok(Value::Template(template)) } /// `v`: Vertical spacing. pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult { - let spacing = args.expect("spacing")?; let mut template = Template::new(); - template.spacing(GenAxis::Block, spacing); + template.spacing(GenAxis::Block, args.expect("spacing")?); Ok(Value::Template(template)) } @@ -196,20 +194,21 @@ pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult { /// `stack`: Stack children along an axis. pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult { enum Child { - Spacing(Linear), + Spacing(Spacing), Any(Template), } castable! { - Child: "linear or template", - Value::Length(v) => Self::Spacing(v.into()), - Value::Relative(v) => Self::Spacing(v.into()), - Value::Linear(v) => Self::Spacing(v), + Child: "linear, fractional or template", + Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())), + Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())), + Value::Linear(v) => Self::Spacing(Spacing::Linear(v)), + Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)), Value::Template(v) => Self::Any(v), } let dir = args.named("dir")?.unwrap_or(Dir::TTB); - let spacing = args.named::("spacing")?; + let spacing = args.named("spacing")?; let list: Vec = args.all().collect(); Ok(Value::Template(Template::from_block(move |style| { @@ -229,7 +228,7 @@ pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult { } let node = template.to_stack(style).into(); - children.push(StackChild::Any(node, style.aligns.block)); + children.push(StackChild::Node(node, style.aligns.block)); delayed = spacing; } } diff --git a/src/library/mod.rs b/src/library/mod.rs index 919f75320..6f8881c3c 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -20,6 +20,7 @@ use crate::diag::{At, TypResult}; use crate::eval::{Args, Array, EvalContext, Scope, Str, Template, Value}; use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::geom::*; +use crate::layout::Spacing; use crate::style::Style; use crate::syntax::{Span, Spanned}; @@ -144,3 +145,11 @@ dynamic! { Value::Relative(v) => Self::Linear(v.into()), Value::Linear(v) => Self::Linear(v), } + +castable! { + Spacing: "linear or fractional", + Value::Length(v) => Self::Linear(v.into()), + Value::Relative(v) => Self::Linear(v.into()), + Value::Linear(v) => Self::Linear(v), + Value::Fractional(v) => Self::Fractional(v), +} diff --git a/tests/ref/layout/spacing.png b/tests/ref/layout/spacing.png index bffa950753f5aec9674cb4a6b938c4a29aa3617c..c653df6d78d3951cb2ddd4abf17009fca1a5cc1d 100644 GIT binary patch literal 1827 zcmZuyc`(}v7>x*iBGROFh0=rw>S)BVsA~2nDHe5A$;PN78rkkzRYe^wZAhys#Hy4m z%~H18t<`2-U8-9pG+L`EamHD9Rjc&S>e|`ao#~8!zIpTJ`{vDj^X9z^z6agZ;ks}L z1fuTYPVt986kwb91+Y0F)xj2Xh2jQs3V8@>{4-m$4Yaw%6zvc&Tss@%XFc>F4?hqkcHW+YqiJO{MHuRLfeHL2AC77%ex><{X(^S+Ap7uU zJvdhwA}m2VU2rO=?M(1^nz#yZT-8L@iWUE2(l#$BfQFTwn!?HkV%aCO++Y;v)a zzUKG13c`vX+C9`dDQhTiP=O2JNAeQIU~pCx#ws#c+p6^l2BL9>Ta=`TLAT@bQa9PF znX2JU2#FtRHuljCXrqavkf`)HzSM&$)oG%eg>*zmukvqQOqOF?*ZG=q*?r z>&y$v5X4>rm|pmjt^!ZLf^UfhLMQnB;#rd}2G(PnXXvEe{WdAi{?g@saxqaVravkd zAg@!Wq#F+oyO!7!!YdmjvW9Etp8XJNpq5cH6;~}Yz!%sP@{P<}7jjviDPC-d#P~PD z%&-xZz$vY0>vfzF&~vlLV!g$CEe2X#^gEEbf`A#Dxrk&3I5?QU~;)Hykh=3?$6ww90#t<*iU6uQ1{8L zrcD84b$1?(C$PEq_Bs*2jxe2m8o^L=d#u!F$+BU5ycGw|?~ZgIfq9v~Fsdj#Rvsa{ zWoKWb zV?28|&}jc#rKrB$b&^wvid_?;y6|a|1-?k=*aG9loClWTbdPXR@kK}b={@TK08%!r z@pGp!0m?n;Ea^J9w5)P zxMBk=^$?A}QsDz~-joPf0O{3s!$_;c@5+yF1FUcjuV1RT^2e@JWu)Va2DD<|04Koy zV~r2p(9rjojitsk@)>T}xJ*z}&sg>V79Uo-n`3OC--7ojq#vkvoZ?SmG4svpT4{Tr zoh(yHcruxv|3(#es9`v!Dt?H3aU)LetF90Z#ggVIZ+9M|qosX%rYq_95G-FT#vexq z!;0FX4r=b4qeDjIh^J*+r^$cpeJj)xQSUC7^aMQ+GgGkM3mCM0>^j?m7IuF~YwP)* z(y>=-E>)*QD#@-1DHe^g$0F2+i1|A!iS||Rs<2(Li!z$dF^$SuH1pMgsIYo$zb(hi zgMWvFdAuJcGU zH-&mW(=o{6I%3Zfx%kjRBw+iDOOEcb^$wUU{-(x7TVcNf?kcfer=XCv`|fB~9w7oh z&?-*ud#$KUNBct?-Ao_(kfUXzzbwznXpVD}+jMNFoK}ZZTFZ?ZDQr?+)9IO}H!rBc zXPPtc7kMGKKafHPYh(T}XkQ_~bOqu+DeQ-cNlak>UTEh)#g9$JnXi>;dR{)fIi{`BLL` zKeiEP%f(IggX4wV1^ literal 1744 zcmZ{leKgaHAICS=^$oM{nmkkF$e?-+svK7W`T^*P@De44I&ez}Y~J#$N1S&P{c>J>A0%p>8-@7=D|SR^H&SAYhy)>~u{d?S7+9-^xDD@}C0W z)0y=rdruaxT|p-yz&oLO>bCEtHSG&3F)cC%)uhe&6wfZ$__Vu3Px7JY?zoCW(UO;t zG=JIz7pU@r{=o^A1C^mArnU|k3&wueU@cJ+`^(fpULINx=v5^kNIJ%Ze{;dSep3>2 z522^;Ma^hIvR6)!)`Mq6R0W1@c0!L%gy*rSCYB9gxRY(Vwa~2c!#$65rQT~Xh?--S&Y#! z*AabajQen@MyNqweAAK8fhKq=9gTkWPO;C$&65)WNaQgkDgi!#B8uFT&d$gDrrzmn z;~lCl9&>Wh^&=SAbE><*NX#C?>9|hw`qNl$9?!}m?Vj49PvD}t#4-%uB-1=lbfxjv zG*2DiH3fW#ca^I^CD|F6VUv?jcG2FP$u$sCbW&1YImsvZOuS3u-qjsSh7GqUEdP0u z5G5A`muW!j>w;&ZV4cFt*w{|l0y9KO>&XJ84U@0`5U@P?Z;1%9era5{c;!Ao>R>$0 z&iJ-60V#DL_m0>$6&iM@^LNBvTi%GLDTaNc6R+?K()dA;!QEiPo>N&OV?yHBm0kj( z`c=!EbyQ#13)$?O_E1u51^i4I5K+i}u!O-W#qs7^1}xNjGk2tRsC64<)3D!Rv>N$KY;tpi zeET_Oc|{L2*;*w8KVI`tYM!h8g6Fz7J<)$+_4sT#^2hBnW^Dn_5+0MFU%-~LJ9YX( zxAff-KZQx)$krh%#f*OXT%YD)3+4uxo{u@0@v5!^KSD1-d|2wzyW5_yja($Esf%)N z@}l!9{8LiFR#@GBoWII4g$qO>B_*7YN<|kScXei8d0Z;_Jp931S|EFO+~& zvRcAVS^C&>IKDnWzc!~8WXSe4ifWi*A=$~M^3Y2-@;~NLxL?N;TByBYssVovTr{PJ zmTYT0HNhxf{Ns#=56kFg7fPi_NK6Bd{TPV5DIc?s+pU#ifgXCP$#DAK9{%4S1%c_u zucKYk5{z*AZ}Qh)0>QlJ1zS?lxbn5se1eL6YTvLbp*`|;#9_1tvGpg3Rk0NQI-KJ;F+q2<>?8u8+P`^tWoPgpx z5s~_S@Vu!cfuO$OeIs3@`$@0KG z!RODaKfup<_cR}T!jSAh${;yZ@!VGy@*y=EV(LW}eX$=>)|`KAcxT26mUFVI70;1r zD*W%oFkm5=yVoixtvfrFAxuah!Hy-Wa>6js9VLd~`&`0$fn5~Uxh#fD#PDtchp{%) zpw*hNoX!0AEL0&OkK!nT`2ICYW@)3O*R0Vz~jwMcK{r=ahB6^VUwYZS< F{{Ry^02u%P diff --git a/tests/ref/layout/stack.png b/tests/ref/layout/stack-1.png similarity index 100% rename from tests/ref/layout/stack.png rename to tests/ref/layout/stack-1.png diff --git a/tests/ref/layout/stack-2.png b/tests/ref/layout/stack-2.png new file mode 100644 index 0000000000000000000000000000000000000000..3e503e653fcd133c685d547c48acf5795932965c GIT binary patch literal 3298 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 literal 0 HcmV?d00001 diff --git a/tests/typ/layout/spacing.typ b/tests/typ/layout/spacing.typ index 3794612cc..7cf6760fa 100644 --- a/tests/typ/layout/spacing.typ +++ b/tests/typ/layout/spacing.typ @@ -14,6 +14,9 @@ Add #h(10pt) #h(10pt) up #let x = 25% - 4pt | #h(x) | #h(x) | #h(x) | #h(x) | +// Fractional. +| #h(1fr) | #h(2fr) | #h(1fr) | + --- // Missing spacing. // Error: 11-13 missing argument: spacing diff --git a/tests/typ/layout/stack.typ b/tests/typ/layout/stack-1.typ similarity index 100% rename from tests/typ/layout/stack.typ rename to tests/typ/layout/stack-1.typ diff --git a/tests/typ/layout/stack-2.typ b/tests/typ/layout/stack-2.typ new file mode 100644 index 000000000..afc9de394 --- /dev/null +++ b/tests/typ/layout/stack-2.typ @@ -0,0 +1,24 @@ +// Test fr units in stacks. + +--- +#page(height: 3.5cm) +#stack( + dir: ltr, + spacing: 1fr, + ..for c in "ABCDEFGHI" {([#c],)} +) + +Hello +#v(2fr) +from #h(1fr) the #h(1fr) wonderful +#v(1fr) +World! 🌍 + +--- +#page(height: 2cm) +#font(white) +#box(fill: forest)[ + #v(1fr) + #h(1fr) Hi you! #h(5pt) + #v(5pt) +]