mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Not too shabby stack layouter 🚆
This commit is contained in:
parent
92586d3e68
commit
d34707a6ae
@ -36,8 +36,6 @@ pub type MultiLayout = Vec<Layout>;
|
|||||||
pub struct Layout {
|
pub struct Layout {
|
||||||
/// The size of the box.
|
/// The size of the box.
|
||||||
pub dimensions: Size2D,
|
pub dimensions: Size2D,
|
||||||
/// The baseline of the layout (as an offset from the top-left).
|
|
||||||
pub baseline: Option<Size>,
|
|
||||||
/// How to align this layout in a parent container.
|
/// How to align this layout in a parent container.
|
||||||
pub alignment: LayoutAlignment,
|
pub alignment: LayoutAlignment,
|
||||||
/// The actions composing this layout.
|
/// The actions composing this layout.
|
||||||
|
@ -70,14 +70,18 @@ impl StackLayouter {
|
|||||||
self.finish_space(true);
|
self.finish_space(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want the new maximal alignment and since the layout's secondary
|
if layout.alignment.secondary == *self.secondary_alignment() {
|
||||||
// alignment is at least the previous maximum, we just take it.
|
// Add a cached soft space if there is one and the alignment stayed
|
||||||
*self.secondary_alignment() = layout.alignment.secondary;
|
// the same. Soft spaces are discarded if the alignment changes.
|
||||||
|
|
||||||
// Add a cached soft space if there is one.
|
|
||||||
if let LastSpacing::Soft(spacing, _) = self.space.last_spacing {
|
if let LastSpacing::Soft(spacing, _) = self.space.last_spacing {
|
||||||
self.add_spacing(spacing, SpacingKind::Hard);
|
self.add_spacing(spacing, SpacingKind::Hard);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// We want the new maximal alignment and since the layout's
|
||||||
|
// secondary alignment is at least the previous maximum, we just
|
||||||
|
// take it.
|
||||||
|
*self.secondary_alignment() = layout.alignment.secondary;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the first space that fits the layout.
|
// Find the first space that fits the layout.
|
||||||
while !self.space.usable.fits(layout.dimensions) {
|
while !self.space.usable.fits(layout.dimensions) {
|
||||||
@ -89,21 +93,7 @@ impl StackLayouter {
|
|||||||
self.finish_space(true);
|
self.finish_space(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let axes = self.ctx.axes;
|
self.update_metrics(layout.dimensions.generalized(self.ctx.axes));
|
||||||
let dimensions = layout.dimensions.generalized(axes);
|
|
||||||
|
|
||||||
let mut size = self.space.size.generalized(axes);
|
|
||||||
let mut extra = self.space.extra.generalized(axes);
|
|
||||||
|
|
||||||
size.x += max(dimensions.x - extra.x, Size::ZERO);
|
|
||||||
size.y += max(dimensions.y - extra.y, Size::ZERO);
|
|
||||||
extra.x = max(extra.x, dimensions.x);
|
|
||||||
extra.y = max(extra.y - dimensions.y, Size::ZERO);
|
|
||||||
|
|
||||||
self.space.size = size.specialized(axes);
|
|
||||||
self.space.extra = extra.specialized(axes);
|
|
||||||
|
|
||||||
*self.space.usable.secondary_mut(axes) -= dimensions.y;
|
|
||||||
|
|
||||||
self.space.layouts.push((self.ctx.axes, layout));
|
self.space.layouts.push((self.ctx.axes, layout));
|
||||||
self.space.last_spacing = LastSpacing::None;
|
self.space.last_spacing = LastSpacing::None;
|
||||||
@ -124,22 +114,24 @@ impl StackLayouter {
|
|||||||
/// Add secondary spacing to the stack.
|
/// Add secondary spacing to the stack.
|
||||||
pub fn add_spacing(&mut self, mut spacing: Size, kind: SpacingKind) {
|
pub fn add_spacing(&mut self, mut spacing: Size, kind: SpacingKind) {
|
||||||
match kind {
|
match kind {
|
||||||
// A hard space is directly added to the sub's size.
|
// A hard space is simply an empty box.
|
||||||
SpacingKind::Hard => {
|
SpacingKind::Hard => {
|
||||||
// Reduce the spacing such that definitely fits.
|
// Reduce the spacing such that it definitely fits.
|
||||||
spacing.min_eq(self.space.usable.secondary(self.ctx.axes));
|
spacing.min_eq(self.space.usable.secondary(self.ctx.axes));
|
||||||
|
let dimensions = Size2D::with_y(spacing);
|
||||||
|
|
||||||
self.add(Layout {
|
self.update_metrics(dimensions);
|
||||||
dimensions: Size2D::with_y(spacing).specialized(self.ctx.axes),
|
|
||||||
baseline: None,
|
self.space.layouts.push((self.ctx.axes, Layout {
|
||||||
|
dimensions: dimensions.specialized(self.ctx.axes),
|
||||||
alignment: LayoutAlignment::default(),
|
alignment: LayoutAlignment::default(),
|
||||||
actions: vec![],
|
actions: vec![]
|
||||||
}).expect("spacing should fit");
|
}));
|
||||||
|
|
||||||
self.space.last_spacing = LastSpacing::Hard;
|
self.space.last_spacing = LastSpacing::Hard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A hard space is cached if it is not consumed by a hard space or
|
// A soft space is cached if it is not consumed by a hard space or
|
||||||
// previous soft space with higher level.
|
// previous soft space with higher level.
|
||||||
SpacingKind::Soft(level) => {
|
SpacingKind::Soft(level) => {
|
||||||
let consumes = match self.space.last_spacing {
|
let consumes = match self.space.last_spacing {
|
||||||
@ -155,11 +147,34 @@ impl StackLayouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the size metrics to reflect that a layout or spacing with the
|
||||||
|
/// given generalized dimensions has been added.
|
||||||
|
fn update_metrics(&mut self, dimensions: Size2D) {
|
||||||
|
let axes = self.ctx.axes;
|
||||||
|
|
||||||
|
let mut size = self.space.size.generalized(axes);
|
||||||
|
let mut extra = self.space.extra.generalized(axes);
|
||||||
|
|
||||||
|
size.x += max(dimensions.x - extra.x, Size::ZERO);
|
||||||
|
size.y += max(dimensions.y - extra.y, Size::ZERO);
|
||||||
|
extra.x = max(extra.x, dimensions.x);
|
||||||
|
extra.y = max(extra.y - dimensions.y, Size::ZERO);
|
||||||
|
|
||||||
|
self.space.size = size.specialized(axes);
|
||||||
|
self.space.extra = extra.specialized(axes);
|
||||||
|
*self.space.usable.secondary_mut(axes) -= dimensions.y;
|
||||||
|
}
|
||||||
|
|
||||||
/// Change the layouting axes used by this layouter.
|
/// Change the layouting axes used by this layouter.
|
||||||
///
|
///
|
||||||
/// This starts a new subspace (if the axes are actually different from the
|
/// This starts a new subspace (if the axes are actually different from the
|
||||||
/// current ones).
|
/// current ones).
|
||||||
pub fn set_axes(&mut self, axes: LayoutAxes) {
|
pub fn set_axes(&mut self, axes: LayoutAxes) {
|
||||||
|
// Forget the spacing because it is not relevant anymore.
|
||||||
|
if axes.secondary != self.ctx.axes.secondary {
|
||||||
|
self.space.last_spacing = LastSpacing::Hard;
|
||||||
|
}
|
||||||
|
|
||||||
self.ctx.axes = axes;
|
self.ctx.axes = axes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,37 +236,105 @@ impl StackLayouter {
|
|||||||
pub fn finish_space(&mut self, hard: bool) {
|
pub fn finish_space(&mut self, hard: bool) {
|
||||||
let space = self.ctx.spaces[self.space.index];
|
let space = self.ctx.spaces[self.space.index];
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 1: Determine the full dimensions of the space.
|
||||||
|
// (Mostly done already while collecting the boxes, but here we
|
||||||
|
// expand if necessary.)
|
||||||
|
|
||||||
let usable = space.usable();
|
let usable = space.usable();
|
||||||
if space.expand.horizontal { self.space.size.x = usable.x; }
|
if space.expand.horizontal { self.space.size.x = usable.x; }
|
||||||
if space.expand.vertical { self.space.size.y = usable.y; }
|
if space.expand.vertical { self.space.size.y = usable.y; }
|
||||||
|
|
||||||
let dimensions = self.space.size.padded(space.padding);
|
let dimensions = self.space.size.padded(space.padding);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 2: Forward pass. Create a bounding box for each layout in which
|
||||||
|
// it will be aligned. Then, go forwards through the boxes and remove
|
||||||
|
// what is taken by previous layouts from the following layouts.
|
||||||
|
|
||||||
|
let start = space.start();
|
||||||
|
|
||||||
|
let mut bounds = vec![];
|
||||||
|
let mut bound = SizeBox {
|
||||||
|
left: start.x,
|
||||||
|
top: start.y,
|
||||||
|
right: start.x + self.space.size.x,
|
||||||
|
bottom: start.y + self.space.size.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (axes, layout) in &self.space.layouts {
|
||||||
|
// First, we store the bounds calculated so far (which were reduced
|
||||||
|
// by the predecessors of this layout) as the initial bounding box
|
||||||
|
// of this layout.
|
||||||
|
bounds.push(bound);
|
||||||
|
|
||||||
|
// Then, we reduce the bounding box for the following layouts. This
|
||||||
|
// layout uses up space from the origin to the end. Thus, it reduces
|
||||||
|
// the usable space for following layouts at it's origin by its
|
||||||
|
// extent along the secondary axis.
|
||||||
|
*bound.secondary_origin_mut(*axes)
|
||||||
|
+= axes.secondary.factor() * layout.dimensions.secondary(*axes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 3: Backward pass. Reduce the bounding boxes from the previous
|
||||||
|
// layouts by what is taken by the following ones.
|
||||||
|
|
||||||
|
let mut extent = Size::ZERO;
|
||||||
|
|
||||||
|
for (bound, entry) in bounds.iter_mut().zip(&self.space.layouts).rev() {
|
||||||
|
let (axes, layout) = entry;
|
||||||
|
|
||||||
|
// We reduce the bounding box of this layout at it's end by the
|
||||||
|
// accumulated secondary extent of all layouts we have seen so far,
|
||||||
|
// which are the layouts after this one since we iterate reversed.
|
||||||
|
*bound.secondary_end_mut(*axes) -= axes.secondary.factor() * extent;
|
||||||
|
|
||||||
|
// Then, we add this layout's secondary extent to the accumulator.
|
||||||
|
extent += layout.dimensions.secondary(*axes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 4: Align each layout in its bounding box and collect everything
|
||||||
|
// into a single finished layout.
|
||||||
|
|
||||||
let mut actions = LayoutActions::new();
|
let mut actions = LayoutActions::new();
|
||||||
actions.add(LayoutAction::DebugBox(dimensions));
|
actions.add(LayoutAction::DebugBox(dimensions));
|
||||||
|
|
||||||
let mut cursor = space.start();
|
let layouts = std::mem::replace(&mut self.space.layouts, vec![]);
|
||||||
for (axes, layout) in std::mem::replace(&mut self.space.layouts, vec![]) {
|
|
||||||
|
for ((axes, layout), bound) in layouts.into_iter().zip(bounds) {
|
||||||
let LayoutAxes { primary, secondary } = axes;
|
let LayoutAxes { primary, secondary } = axes;
|
||||||
|
|
||||||
let size = layout.dimensions.specialized(axes);
|
let size = layout.dimensions.specialized(axes);
|
||||||
let alignment = layout.alignment.primary;
|
let alignment = layout.alignment;
|
||||||
|
|
||||||
let primary_usable = self.space.size.primary(axes) - cursor.primary(axes);
|
// The space in which this layout is aligned is given by it's
|
||||||
|
// corresponding bound box.
|
||||||
|
let usable = Size2D::new(
|
||||||
|
bound.right - bound.left,
|
||||||
|
bound.bottom - bound.top
|
||||||
|
).generalized(axes);
|
||||||
|
|
||||||
let position = Size2D {
|
let offsets = Size2D {
|
||||||
x: cursor.primary(axes)
|
x: usable.x.anchor(alignment.primary, primary.is_positive())
|
||||||
+ primary_usable.anchor(alignment, primary.is_positive())
|
- size.x.anchor(alignment.primary, primary.is_positive()),
|
||||||
- size.x.anchor(alignment, primary.is_positive()),
|
y: usable.y.anchor(alignment.secondary, secondary.is_positive())
|
||||||
y: cursor.secondary(axes),
|
- size.y.anchor(alignment.secondary, secondary.is_positive()),
|
||||||
};
|
};
|
||||||
|
|
||||||
actions.add_layout(position.specialized(axes), layout);
|
let position = Size2D::new(bound.left, bound.top)
|
||||||
*cursor.secondary_mut(axes) += size.y;
|
+ offsets.specialized(axes);
|
||||||
|
|
||||||
|
println!("pos: {}", position);
|
||||||
|
println!("usable: {}", usable);
|
||||||
|
println!("size: {}", size);
|
||||||
|
|
||||||
|
actions.add_layout(position, layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.layouts.push(Layout {
|
self.layouts.push(Layout {
|
||||||
dimensions,
|
dimensions,
|
||||||
baseline: None,
|
|
||||||
alignment: self.ctx.alignment,
|
alignment: self.ctx.alignment,
|
||||||
actions: actions.to_vec(),
|
actions: actions.to_vec(),
|
||||||
});
|
});
|
||||||
|
@ -73,7 +73,6 @@ impl<'a, 'p> TextLayouter<'a, 'p> {
|
|||||||
|
|
||||||
Ok(Layout {
|
Ok(Layout {
|
||||||
dimensions: Size2D::new(self.width, self.ctx.style.font_size),
|
dimensions: Size2D::new(self.width, self.ctx.style.font_size),
|
||||||
baseline: None,
|
|
||||||
alignment: self.ctx.alignment,
|
alignment: self.ctx.alignment,
|
||||||
actions: self.actions.to_vec(),
|
actions: self.actions.to_vec(),
|
||||||
})
|
})
|
||||||
|
28
src/size.rs
28
src/size.rs
@ -6,7 +6,7 @@ use std::iter::Sum;
|
|||||||
use std::ops::*;
|
use std::ops::*;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::layout::{LayoutAxes, Alignment};
|
use crate::layout::{LayoutAxes, Axis, Alignment};
|
||||||
|
|
||||||
/// A general space type.
|
/// A general space type.
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
@ -101,7 +101,6 @@ impl Size {
|
|||||||
(true, End) | (false, Origin) => *self,
|
(true, End) | (false, Origin) => *self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Size2D {
|
impl Size2D {
|
||||||
@ -219,6 +218,11 @@ impl Size2D {
|
|||||||
self.x.min_eq(other.x);
|
self.x.min_eq(other.x);
|
||||||
self.y.min_eq(other.y);
|
self.y.min_eq(other.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Swap the two dimensions.
|
||||||
|
pub fn swap(&mut self) {
|
||||||
|
std::mem::swap(&mut self.x, &mut self.y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizeBox {
|
impl SizeBox {
|
||||||
@ -250,6 +254,26 @@ impl SizeBox {
|
|||||||
SizeBox { left: value, top: value, right: value, bottom: value }
|
SizeBox { left: value, top: value, right: value, bottom: value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the origin direction on the secondary axis of this box.
|
||||||
|
pub fn secondary_origin_mut(&mut self, axes: LayoutAxes) -> &mut Size {
|
||||||
|
match axes.secondary {
|
||||||
|
Axis::LeftToRight => &mut self.left,
|
||||||
|
Axis::RightToLeft => &mut self.right,
|
||||||
|
Axis::TopToBottom => &mut self.top,
|
||||||
|
Axis::BottomToTop => &mut self.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the end direction on the secondary axis of this box.
|
||||||
|
pub fn secondary_end_mut(&mut self, axes: LayoutAxes) -> &mut Size {
|
||||||
|
match axes.secondary {
|
||||||
|
Axis::LeftToRight => &mut self.right,
|
||||||
|
Axis::RightToLeft => &mut self.left,
|
||||||
|
Axis::TopToBottom => &mut self.bottom,
|
||||||
|
Axis::BottomToTop => &mut self.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the `left` and `right` values.
|
/// Set the `left` and `right` values.
|
||||||
pub fn set_all(&mut self, value: Size) {
|
pub fn set_all(&mut self, value: Size) {
|
||||||
*self = SizeBox::with_all(value);
|
*self = SizeBox::with_all(value);
|
||||||
|
@ -13,9 +13,40 @@
|
|||||||
// ]
|
// ]
|
||||||
|
|
||||||
// Test 2
|
// Test 2
|
||||||
[align: secondary=top] Top
|
// [align: secondary=top] Top
|
||||||
[align: secondary=center] Center
|
// [align: secondary=center] Center
|
||||||
[align: secondary=bottom] Bottom
|
// [align: secondary=bottom] Bottom
|
||||||
[direction: ttb, ltr]
|
// [direction: ttb, ltr]
|
||||||
[align: primary=bottom]
|
// [align: primary=bottom]
|
||||||
[box: w=1cm, h=1cm]
|
// [box: w=1cm, h=1cm]
|
||||||
|
|
||||||
|
// Test 3
|
||||||
|
// [align: center][
|
||||||
|
// Somelongspacelessword!
|
||||||
|
// [align: left] Some
|
||||||
|
// [align: right] word!
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// Test 4: In all combinations, please!
|
||||||
|
// [direction: ltr, ttb]
|
||||||
|
// [align: center]
|
||||||
|
// [align: secondary=origin]
|
||||||
|
// [box: ps=1cm, ss=1cm]
|
||||||
|
// [align: secondary=center]
|
||||||
|
// [box: ps=3cm, ss=1cm]
|
||||||
|
// [box: ps=4cm, ss=0.5cm]
|
||||||
|
// [align: secondary=end]
|
||||||
|
// [box: ps=2cm, ss=1cm]
|
||||||
|
|
||||||
|
[align: primary=left, secondary=center]
|
||||||
|
[box: w=4cm, h=2cm]
|
||||||
|
|
||||||
|
[direction: primary=btt, secondary=ltr]
|
||||||
|
[align: primary=center, secondary=left]
|
||||||
|
[box: h=2cm, w=1cm]
|
||||||
|
|
||||||
|
// [direction: rtl, btt]
|
||||||
|
// [align: center]
|
||||||
|
// [align: vertical=origin] ORIGIN
|
||||||
|
// [align: vertical=center] CENTER
|
||||||
|
// [align: vertical=end] END
|
||||||
|
Loading…
x
Reference in New Issue
Block a user