diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 47dd93eac..6ff287f08 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -1,201 +1,34 @@ use super::*; /// A node that stacks and aligns its children. -/// -/// # Alignment -/// Individual layouts can be aligned at `Start`, `Center` or `End` along both -/// axes. These alignments are with processed with respect to the size of the -/// finished layout and not the total usable size. This means that a later -/// layout can have influence on the position of an earlier one. Consider the -/// following example. -/// ```typst -/// [align: right][A word.] -/// [align: left][A sentence with a couple more words.] -/// ``` -/// The resulting layout looks like this: -/// ```text -/// |--------------------------------------| -/// | A word. | -/// | | -/// | A sentence with a couple more words. | -/// |--------------------------------------| -/// ``` -/// The position of the first aligned box thus depends on the length of the -/// sentence in the second box. #[derive(Debug, Clone, PartialEq)] pub struct Stack { + /// The `main` and `cross` directions of this stack. + /// + /// The children are stacked along the `main` direction. The `cross` + /// direction is required for aligning the children. pub dirs: Gen, - pub children: Vec, + /// How to align _this_ stack in _its_ parent. pub aligns: Gen, - pub expand: Spec, + /// Whether to expand the axes to fill the area or to fit the content. + pub expansion: Gen, + /// The nodes to be stacked. + pub children: Vec, } #[async_trait(?Send)] impl Layout for Stack { - async fn layout( - &self, - ctx: &mut LayoutContext, - constraints: LayoutConstraints, - ) -> Vec { - let mut items = vec![]; - - let size = constraints.spaces[0].size; - let mut space = StackSpace::new(self.dirs, self.expand, size); - let mut i = 0; - + async fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec { + let mut layouter = StackLayouter::new(self, areas.clone()); for child in &self.children { - let child_constraints = LayoutConstraints { - spaces: { - let mut remaining = vec![LayoutSpace { - base: space.full_size, - size: space.usable, - }]; - let next = (i + 1).min(constraints.spaces.len() - 1); - remaining.extend(&constraints.spaces[next ..]); - remaining - }, - repeat: constraints.repeat, - }; - - for item in child.layout(ctx, child_constraints).await { - match item { - Layouted::Spacing(spacing) => space.push_spacing(spacing), - Layouted::Box(mut boxed, aligns) => { - let mut last = false; - while let Err(back) = space.push_box(boxed, aligns) { - boxed = back; - if last { - break; - } - - items.push(Layouted::Box(space.finish(), self.aligns)); - - if i + 1 < constraints.spaces.len() { - i += 1; - } else { - last = true; - } - - let size = constraints.spaces[i].size; - space = StackSpace::new(self.dirs, self.expand, size); - } - } + for layouted in child.layout(ctx, &layouter.areas).await { + match layouted { + Layouted::Spacing(spacing) => layouter.spacing(spacing), + Layouted::Boxed(boxed, aligns) => layouter.boxed(boxed, aligns), } } } - - items.push(Layouted::Box(space.finish(), self.aligns)); - items - } -} - -struct StackSpace { - dirs: Gen, - expand: Spec, - boxes: Vec<(BoxLayout, Gen)>, - full_size: Size, - usable: Size, - used: Size, - ruler: Align, -} - -impl StackSpace { - fn new(dirs: Gen, expand: Spec, size: Size) -> Self { - Self { - dirs, - expand, - boxes: vec![], - full_size: size, - usable: size, - used: Size::ZERO, - ruler: Align::Start, - } - } - - fn push_box( - &mut self, - boxed: BoxLayout, - aligns: Gen, - ) -> Result<(), BoxLayout> { - let main = self.dirs.main.axis(); - let cross = self.dirs.cross.axis(); - if aligns.main < self.ruler || !self.usable.fits(boxed.size) { - return Err(boxed); - } - - let size = boxed.size.switch(self.dirs); - *self.used.get_mut(cross) = self.used.get(cross).max(size.cross); - *self.used.get_mut(main) += size.main; - *self.usable.get_mut(main) -= size.main; - self.boxes.push((boxed, aligns)); - self.ruler = aligns.main; - - Ok(()) - } - - fn push_spacing(&mut self, spacing: Length) { - let main = self.dirs.main.axis(); - let max = self.usable.get(main); - let trimmed = spacing.min(max); - *self.used.get_mut(main) += trimmed; - *self.usable.get_mut(main) -= trimmed; - - let size = Gen::new(trimmed, Length::ZERO).switch(self.dirs); - self.boxes.push((BoxLayout::new(size.to_size()), Gen::default())); - } - - fn finish(mut self) -> BoxLayout { - let dirs = self.dirs; - let main = dirs.main.axis(); - - if self.expand.horizontal { - self.used.width = self.full_size.width; - } - - if self.expand.vertical { - self.used.height = self.full_size.height; - } - - let mut sum = Length::ZERO; - let mut sums = Vec::with_capacity(self.boxes.len() + 1); - - for (boxed, _) in &self.boxes { - sums.push(sum); - sum += boxed.size.get(main); - } - - sums.push(sum); - - let mut layout = BoxLayout::new(self.used); - let used = self.used.switch(dirs); - - for (i, (boxed, aligns)) in self.boxes.into_iter().enumerate() { - let size = boxed.size.switch(dirs); - - let before = sums[i]; - let after = sum - sums[i + 1]; - let main_len = used.main - size.main; - let main_range = if dirs.main.is_positive() { - before .. main_len - after - } else { - main_len - before .. after - }; - - let cross_len = used.cross - size.cross; - let cross_range = if dirs.cross.is_positive() { - Length::ZERO .. cross_len - } else { - cross_len .. Length::ZERO - }; - - let main = aligns.main.apply(main_range); - let cross = aligns.cross.apply(cross_range); - let pos = Gen::new(main, cross).switch(dirs).to_point(); - - layout.push_layout(pos, boxed); - } - - layout + layouter.finish() } } @@ -204,3 +37,113 @@ impl From for LayoutNode { Self::dynamic(stack) } } + +struct StackLayouter<'a> { + stack: &'a Stack, + main: SpecAxis, + dirs: Gen, + areas: Areas, + layouted: Vec, + boxes: Vec<(Length, BoxLayout, Gen)>, + used: Gen, + ruler: Align, +} + +impl<'a> StackLayouter<'a> { + fn new(stack: &'a Stack, areas: Areas) -> Self { + Self { + stack, + main: stack.dirs.main.axis(), + dirs: stack.dirs, + areas, + layouted: vec![], + boxes: vec![], + used: Gen::ZERO, + ruler: Align::Start, + } + } + + fn spacing(&mut self, amount: Length) { + let main_rest = self.areas.current.rem.get_mut(self.main); + let capped = amount.min(*main_rest); + *main_rest -= capped; + self.used.main += capped; + } + + fn boxed(&mut self, layout: BoxLayout, aligns: Gen) { + if self.ruler > aligns.main { + self.finish_area(); + } + + while !self.areas.current.rem.fits(layout.size) { + if self.areas.in_full_last() { + // TODO: Diagnose once the necessary spans exist. + let _ = warning!("cannot fit box into any area"); + break; + } else { + self.finish_area(); + } + } + + let size = layout.size.switch(self.dirs); + self.boxes.push((self.used.main, layout, aligns)); + + *self.areas.current.rem.get_mut(self.main) -= size.main; + self.used.main += size.main; + self.used.cross = self.used.cross.max(size.cross); + self.ruler = aligns.main; + } + + fn finish_area(&mut self) { + let size = { + let full = self.areas.current.full.switch(self.dirs); + Gen::new( + match self.stack.expansion.main { + Expansion::Fill => full.main, + Expansion::Fit => self.used.main.min(full.main), + }, + match self.stack.expansion.cross { + Expansion::Fill => full.cross, + Expansion::Fit => self.used.cross.min(full.cross), + }, + ) + }; + + let mut output = BoxLayout::new(size.switch(self.dirs).to_size()); + + for (before, layout, aligns) in std::mem::take(&mut self.boxes) { + let child_size = layout.size.switch(self.dirs); + + // Align along the main axis. + let main = aligns.main.apply(if self.dirs.main.is_positive() { + let after_with_self = self.used.main - before; + before .. size.main - after_with_self + } else { + let before_with_self = before + child_size.main; + let after = self.used.main - (before + child_size.main); + size.main - before_with_self .. after + }); + + // Align along the cross axis. + let cross = aligns.cross.apply(if self.dirs.cross.is_positive() { + Length::ZERO .. size.cross - child_size.cross + } else { + size.cross - child_size.cross .. Length::ZERO + }); + + let pos = Gen::new(main, cross).switch(self.dirs).to_point(); + output.push_layout(pos, layout); + } + + self.layouted.push(Layouted::Boxed(output, self.stack.aligns)); + + self.areas.next(); + self.used = Gen::ZERO; + self.ruler = Align::Start; + } + + fn finish(mut self) -> Vec { + self.finish_area(); + self.layouted + } +}