mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
443 lines
14 KiB
Rust
443 lines
14 KiB
Rust
use smallvec::smallvec;
|
|
use super::*;
|
|
|
|
/// The stack layouter arranges boxes stacked onto each other.
|
|
///
|
|
/// The boxes are laid out in the direction of the secondary layouting axis and
|
|
/// are aligned along both axes.
|
|
#[derive(Debug, Clone)]
|
|
pub struct StackLayouter {
|
|
/// The context for layouter.
|
|
ctx: StackContext,
|
|
/// The output layouts.
|
|
layouts: MultiLayout,
|
|
/// The currently active layout space.
|
|
space: Space,
|
|
/// The remaining subspace of the active space. Whenever the layouting axes
|
|
/// change a new subspace is started.
|
|
sub: Subspace,
|
|
}
|
|
|
|
/// The context for stack layouting.
|
|
///
|
|
/// See [`LayoutContext`] for details about the fields.
|
|
#[derive(Debug, Clone)]
|
|
pub struct StackContext {
|
|
pub spaces: LayoutSpaces,
|
|
pub axes: LayoutAxes,
|
|
pub alignment: LayoutAlignment,
|
|
}
|
|
|
|
/// A layout space composed of subspaces which can have different axes and
|
|
/// alignments.
|
|
#[derive(Debug, Clone)]
|
|
struct Space {
|
|
/// The index of this space in the list of spaces.
|
|
index: usize,
|
|
/// Whether to add the layout for this space even if it would be empty.
|
|
hard: bool,
|
|
/// The so-far accumulated subspaces.
|
|
subs: Vec<Subspace>,
|
|
}
|
|
|
|
/// A part of a space with fixed axes and secondary alignment.
|
|
#[derive(Debug, Clone)]
|
|
struct Subspace {
|
|
/// The axes along which contents in this subspace are laid out.
|
|
axes: LayoutAxes,
|
|
/// The secondary alignment of this subspace.
|
|
alignment: Alignment,
|
|
/// The beginning of this subspace in the parent space (specialized).
|
|
origin: Size2D,
|
|
/// The total usable space of this subspace (generalized).
|
|
usable: Size2D,
|
|
/// The used size of this subspace (generalized), with
|
|
/// - `x` being the maximum of the primary size of all boxes.
|
|
/// - `y` being the total extent of all boxes and space in the secondary
|
|
/// direction.
|
|
size: Size2D,
|
|
/// The so-far accumulated layouts.
|
|
layouts: Vec<LayoutEntry>,
|
|
/// The last added spacing if the last added thing was spacing.
|
|
last_spacing: LastSpacing,
|
|
}
|
|
|
|
/// A single layout in a subspace.
|
|
#[derive(Debug, Clone)]
|
|
struct LayoutEntry {
|
|
/// The offset of this box on the secondary axis.
|
|
offset: Size,
|
|
/// The layout itself.
|
|
layout: Layout,
|
|
}
|
|
|
|
impl StackLayouter {
|
|
/// Create a new stack layouter.
|
|
pub fn new(ctx: StackContext) -> StackLayouter {
|
|
let axes = ctx.axes;
|
|
let space = ctx.spaces[0];
|
|
|
|
StackLayouter {
|
|
ctx,
|
|
layouts: MultiLayout::new(),
|
|
space: Space::new(0, true),
|
|
sub: Subspace::new(axes, Alignment::Origin, space.start(), space.usable()),
|
|
}
|
|
}
|
|
|
|
/// Add a layout to the stack.
|
|
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
|
|
if layout.alignment.secondary != self.sub.alignment {
|
|
self.finish_subspace(layout.alignment.secondary);
|
|
}
|
|
|
|
// Add a cached soft space if there is one.
|
|
if let LastSpacing::Soft(space, _) = self.sub.last_spacing {
|
|
self.add_spacing(space, SpacingKind::Hard);
|
|
}
|
|
|
|
// The new primary size is the maximum of the current one and the
|
|
// layout's one while the secondary size grows by the layout's size.
|
|
let size = self.ctx.axes.generalize(layout.dimensions);
|
|
let mut new_size = Size2D {
|
|
x: crate::size::max(self.sub.size.x, size.x),
|
|
y: self.sub.size.y + size.y
|
|
};
|
|
|
|
// Find the first (sub-)space that fits the layout.
|
|
while !self.sub.usable.fits(new_size) {
|
|
if self.space_is_last() && self.space_is_empty() {
|
|
error!("box of size {} does not fit into remaining stack of size {}",
|
|
size, self.sub.usable - Size2D::with_y(self.sub.size.y));
|
|
}
|
|
|
|
self.finish_space(true);
|
|
new_size = size;
|
|
}
|
|
|
|
// The secondary offset from the start of layouts is given by the
|
|
// current primary size of the subspace.
|
|
let offset = self.sub.size.y;
|
|
self.sub.layouts.push(LayoutEntry {
|
|
offset,
|
|
layout,
|
|
});
|
|
|
|
// The new size of the subspace is the previously calculated
|
|
// combination.
|
|
self.sub.size = new_size;
|
|
|
|
// Since the last item was a box, last spacing is reset to `None`.
|
|
self.sub.last_spacing = LastSpacing::None;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add multiple layouts to the stack.
|
|
///
|
|
/// This function simply calls `add` for each layout.
|
|
pub fn add_multiple(&mut self, layouts: MultiLayout) -> LayoutResult<()> {
|
|
for layout in layouts {
|
|
self.add(layout)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Add secondary spacing to the stack.
|
|
pub fn add_spacing(&mut self, space: Size, kind: SpacingKind) {
|
|
match kind {
|
|
// A hard space is directly added to the sub's size.
|
|
SpacingKind::Hard => {
|
|
if self.sub.size.y + space > self.sub.usable.y {
|
|
self.sub.size.y = self.sub.usable.y;
|
|
} else {
|
|
self.sub.size.y += space;
|
|
}
|
|
|
|
self.sub.last_spacing = LastSpacing::Hard;
|
|
}
|
|
|
|
// A hard space is cached if it is not consumed by a hard space or
|
|
// previous soft space with higher level.
|
|
SpacingKind::Soft(level) => {
|
|
let consumes = match self.sub.last_spacing {
|
|
LastSpacing::None => true,
|
|
LastSpacing::Soft(_, prev) if level < prev => true,
|
|
_ => false,
|
|
};
|
|
|
|
if consumes {
|
|
self.sub.last_spacing = LastSpacing::Soft(space, level);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Change the layouting axes used by this layouter.
|
|
///
|
|
/// This starts a new subspace (if the axes are actually different from the
|
|
/// current ones).
|
|
pub fn set_axes(&mut self, axes: LayoutAxes) {
|
|
if axes != self.ctx.axes {
|
|
self.finish_subspace(Alignment::Origin);
|
|
|
|
let (origin, usable) = self.remaining_subspace();
|
|
self.sub = Subspace::new(axes, Alignment::Origin, origin, usable);
|
|
self.ctx.axes = axes;
|
|
}
|
|
}
|
|
|
|
/// Change the layouting spaces to use.
|
|
///
|
|
/// If `replace_empty` is true, the current space is replaced if there are
|
|
/// no boxes laid into it yet. Otherwise, only the followup spaces are
|
|
/// replaced.
|
|
pub fn set_spaces(&mut self, spaces: LayoutSpaces, replace_empty: bool) {
|
|
if replace_empty && self.space_is_empty() {
|
|
self.ctx.spaces = spaces;
|
|
self.start_space(0, self.space.hard);
|
|
} else {
|
|
self.ctx.spaces.truncate(self.space.index + 1);
|
|
self.ctx.spaces.extend(spaces);
|
|
}
|
|
}
|
|
|
|
/// The remaining unpadded, unexpanding spaces. If a multi-layout is laid
|
|
/// out into these spaces, it will fit into this stack.
|
|
pub fn remaining(&self) -> LayoutSpaces {
|
|
let mut spaces = smallvec![LayoutSpace {
|
|
dimensions: self.remaining_subspace().1,
|
|
padding: SizeBox::zero(),
|
|
expand: (false, false),
|
|
}];
|
|
|
|
for space in &self.ctx.spaces[self.next_space()..] {
|
|
spaces.push(space.usable_space());
|
|
}
|
|
|
|
spaces
|
|
}
|
|
|
|
/// The usable size along the primary axis.
|
|
pub fn primary_usable(&self) -> Size {
|
|
self.sub.usable.x
|
|
}
|
|
|
|
/// Whether the current layout space (not subspace) is empty.
|
|
pub fn space_is_empty(&self) -> bool {
|
|
self.subspace_is_empty() && self.space.subs.is_empty()
|
|
}
|
|
|
|
/// Whether the current layout space is the last is the followup list.
|
|
pub fn space_is_last(&self) -> bool {
|
|
self.space.index == self.ctx.spaces.len() - 1
|
|
}
|
|
|
|
/// Compute the finished multi-layout.
|
|
pub fn finish(mut self) -> MultiLayout {
|
|
if self.space.hard || !self.space_is_empty() {
|
|
self.finish_space(false);
|
|
}
|
|
self.layouts
|
|
}
|
|
|
|
/// Finish the current space and start a new one.
|
|
pub fn finish_space(&mut self, hard: bool) {
|
|
self.finish_subspace(Alignment::Origin);
|
|
|
|
println!();
|
|
println!("FINISHING SPACE:");
|
|
println!();
|
|
|
|
let space = self.ctx.spaces[self.space.index];
|
|
let mut subs = std::mem::replace(&mut self.space.subs, vec![]);
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Compute the size of the whole space.
|
|
let usable = space.usable();
|
|
let mut max = Size2D {
|
|
x: if space.expand.0 { usable.x } else { Size::zero() },
|
|
y: if space.expand.1 { usable.y } else { Size::zero() },
|
|
};
|
|
|
|
// The total size is determined by the maximum position + extent of one
|
|
// of the boxes.
|
|
for sub in &subs {
|
|
max.max_eq(sub.origin + sub.axes.specialize(sub.size));
|
|
}
|
|
|
|
let dimensions = max.padded(space.padding);
|
|
|
|
println!("WITH DIMENSIONS: {}", dimensions);
|
|
|
|
println!("SUBS: {:#?}", subs);
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Justify the boxes according to their alignment and give each box
|
|
// the appropriate origin and usable space.
|
|
|
|
// use Alignment::*;
|
|
|
|
for sub in &mut subs {
|
|
// The usable width should not exceed the total usable width
|
|
// (previous value) or the maximum width of the layout as a whole.
|
|
sub.usable.x = crate::size::min(
|
|
sub.usable.x,
|
|
sub.axes.specialize(max - sub.origin).x,
|
|
);
|
|
|
|
sub.usable.y = sub.size.y;
|
|
}
|
|
|
|
// if space.expand.1 {
|
|
// let height = subs.iter().map(|sub| sub.size.y).sum();
|
|
// let centers = subs.iter()
|
|
// .filter(|sub| sub.alignment == Alignment::Center)
|
|
// .count()
|
|
// .max(1);
|
|
|
|
// let grow = max.y - height;
|
|
// let center_grow = grow / (centers as i32);
|
|
|
|
// println!("center grow = {}", center_grow);
|
|
|
|
// let mut offset = Size::zero();
|
|
// for sub in &mut subs {
|
|
// sub.origin.y += offset;
|
|
// if sub.alignment == Center {
|
|
// sub.usable.y += center_grow;
|
|
// offset += center_grow;
|
|
// }
|
|
// }
|
|
|
|
// if let Some(last) = subs.last_mut() {
|
|
// last.usable.y += grow - offset;
|
|
// }
|
|
// }
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Do the thing
|
|
|
|
// Add a debug box with this boxes size.
|
|
let mut actions = LayoutActions::new();
|
|
actions.add(LayoutAction::DebugBox(dimensions));
|
|
|
|
for sub in subs {
|
|
let LayoutAxes { primary, secondary } = sub.axes;
|
|
|
|
// The factor is +1 if the axis is positive and -1 otherwise.
|
|
let factor = sub.axes.secondary.factor();
|
|
|
|
// The anchor is the position of the origin-most point of the
|
|
// layout.
|
|
let anchor =
|
|
sub.usable.y.anchor(sub.alignment, secondary.is_positive())
|
|
- factor * sub.size.y.anchor(sub.alignment, true);
|
|
|
|
for entry in sub.layouts {
|
|
let layout = entry.layout;
|
|
let alignment = layout.alignment.primary;
|
|
let size = sub.axes.generalize(layout.dimensions);
|
|
|
|
let x =
|
|
sub.usable.x.anchor(alignment, primary.is_positive())
|
|
- size.x.anchor(alignment, primary.is_positive());
|
|
|
|
let y = anchor
|
|
+ factor * entry.offset
|
|
- size.y.anchor(Alignment::Origin, secondary.is_positive());
|
|
|
|
let pos = sub.origin + sub.axes.specialize(Size2D::new(x, y));
|
|
actions.add_layout(pos, layout);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
self.layouts.push(Layout {
|
|
dimensions,
|
|
baseline: None,
|
|
alignment: self.ctx.alignment,
|
|
actions: actions.to_vec(),
|
|
});
|
|
|
|
self.start_space(self.next_space(), hard);
|
|
}
|
|
|
|
/// Start a new space with the given index.
|
|
fn start_space(&mut self, space: usize, hard: bool) {
|
|
// Start the space.
|
|
self.space = Space::new(space, hard);
|
|
|
|
// Start the subspace.
|
|
let space = self.ctx.spaces[space];
|
|
let axes = self.ctx.axes;
|
|
self.sub = Subspace::new(axes, Alignment::Origin, space.start(), space.usable());
|
|
}
|
|
|
|
/// The index of the next space.
|
|
fn next_space(&self) -> usize {
|
|
(self.space.index + 1).min(self.ctx.spaces.len() - 1)
|
|
}
|
|
|
|
/// Finish the current subspace.
|
|
fn finish_subspace(&mut self, new_alignment: Alignment) {
|
|
let empty = self.subspace_is_empty();
|
|
|
|
let axes = self.ctx.axes;
|
|
let (origin, usable) = self.remaining_subspace();
|
|
let new_sub = Subspace::new(axes, new_alignment, origin, usable);
|
|
let sub = std::mem::replace(&mut self.sub, new_sub);
|
|
|
|
if !empty {
|
|
self.space.subs.push(sub);
|
|
}
|
|
}
|
|
|
|
/// The remaining sub
|
|
fn remaining_subspace(&self) -> (Size2D, Size2D) {
|
|
let offset = self.sub.size.y + self.sub.last_spacing.soft_or_zero();
|
|
|
|
let new_origin = self.sub.origin + match self.ctx.axes.secondary.is_positive() {
|
|
true => self.ctx.axes.specialize(Size2D::with_y(offset)),
|
|
false => Size2D::zero(),
|
|
};
|
|
|
|
let new_usable = self.ctx.axes.specialize(Size2D {
|
|
x: self.sub.usable.x,
|
|
y: self.sub.usable.y - offset,
|
|
});
|
|
|
|
(new_origin, new_usable)
|
|
}
|
|
|
|
/// Whether the current layout space (not subspace) is empty.
|
|
fn subspace_is_empty(&self) -> bool {
|
|
self.sub.layouts.is_empty() && self.sub.size == Size2D::zero()
|
|
}
|
|
}
|
|
|
|
impl Space {
|
|
fn new(index: usize, hard: bool) -> Space {
|
|
Space {
|
|
index,
|
|
hard,
|
|
subs: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Subspace {
|
|
fn new(axes: LayoutAxes, alignment: Alignment, origin: Size2D, usable: Size2D) -> Subspace {
|
|
Subspace {
|
|
axes,
|
|
alignment,
|
|
origin,
|
|
usable: axes.generalize(usable),
|
|
size: Size2D::zero(),
|
|
layouts: vec![],
|
|
last_spacing: LastSpacing::Hard,
|
|
}
|
|
}
|
|
}
|