408 lines
15 KiB
Rust
408 lines
15 KiB
Rust
use smallvec::smallvec;
|
|
use crate::size::ValueBox;
|
|
use super::*;
|
|
|
|
|
|
/// The stack layouter stack boxes onto each other along the secondary layouting
|
|
/// axis.
|
|
#[derive(Debug, Clone)]
|
|
pub struct StackLayouter {
|
|
/// The context for layouting.
|
|
ctx: StackContext,
|
|
/// The output layouts.
|
|
layouts: MultiLayout,
|
|
/// The currently active layout space.
|
|
space: Space,
|
|
}
|
|
|
|
/// The context for stack layouting.
|
|
#[derive(Debug, Clone)]
|
|
pub struct StackContext {
|
|
/// The spaces to layout in.
|
|
pub spaces: LayoutSpaces,
|
|
/// The initial layouting axes, which can be updated by the
|
|
/// [`StackLayouter::set_axes`] method.
|
|
pub axes: LayoutAxes,
|
|
/// Which alignment to set on the resulting layout. This affects how it will
|
|
/// be positioned in a parent box.
|
|
pub alignment: LayoutAlignment,
|
|
/// Whether to have repeated spaces or to use only the first and only once.
|
|
pub repeat: bool,
|
|
/// Whether to output a command which renders a debugging box showing the
|
|
/// extent of the layout.
|
|
pub debug: bool,
|
|
}
|
|
|
|
/// 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 layouts.
|
|
layouts: Vec<(LayoutAxes, Layout)>,
|
|
/// The specialized size of this space.
|
|
size: Size2D,
|
|
/// The specialized remaining space.
|
|
usable: Size2D,
|
|
/// The specialized extra-needed dimensions to affect the size at all.
|
|
extra: Size2D,
|
|
/// The rulers of a space dictate which alignments for new boxes are still
|
|
/// allowed and which require a new space to be started.
|
|
rulers: ValueBox<Alignment>,
|
|
/// The last added spacing if the last added thing was spacing.
|
|
last_spacing: LastSpacing,
|
|
}
|
|
|
|
impl StackLayouter {
|
|
/// Create a new stack layouter.
|
|
pub fn new(ctx: StackContext) -> StackLayouter {
|
|
let space = ctx.spaces[0];
|
|
StackLayouter {
|
|
ctx,
|
|
layouts: MultiLayout::new(),
|
|
space: Space::new(0, true, space.usable()),
|
|
}
|
|
}
|
|
|
|
/// Add a layout to the stack.
|
|
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
|
|
// If the alignment cannot be fit in this space, finish it.
|
|
if !self.update_rulers(layout.alignment) {
|
|
self.finish_space(true)?;
|
|
}
|
|
|
|
// Now, we add a possibly cached soft space. If the secondary alignment
|
|
// changed before, a possibly cached space would have already been
|
|
// discarded.
|
|
if let LastSpacing::Soft(spacing, _) = self.space.last_spacing {
|
|
self.add_spacing(spacing, SpacingKind::Hard);
|
|
}
|
|
|
|
// Find the first space that fits the layout.
|
|
while !self.space.usable.fits(layout.dimensions) {
|
|
if self.space_is_last() && self.space_is_empty() {
|
|
error!("cannot fit box of size {} into usable size of {}",
|
|
layout.dimensions, self.space.usable);
|
|
}
|
|
|
|
self.finish_space(true)?;
|
|
}
|
|
|
|
// Change the usable space and size of the space.
|
|
self.update_metrics(layout.dimensions.generalized(self.ctx.axes));
|
|
|
|
// Add the box to the vector and remember that spacings are allowed
|
|
// again.
|
|
self.space.layouts.push((self.ctx.axes, layout));
|
|
self.space.last_spacing = LastSpacing::None;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add multiple layouts to the stack.
|
|
///
|
|
/// This function simply calls `add` repeatedly 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, mut spacing: Size, kind: SpacingKind) {
|
|
match kind {
|
|
// A hard space is simply an empty box.
|
|
SpacingKind::Hard => {
|
|
// Reduce the spacing such that it definitely fits.
|
|
spacing.min_eq(self.space.usable.get_secondary(self.ctx.axes));
|
|
let dimensions = Size2D::with_y(spacing);
|
|
|
|
self.update_metrics(dimensions);
|
|
self.space.layouts.push((self.ctx.axes, Layout {
|
|
dimensions: dimensions.specialized(self.ctx.axes),
|
|
alignment: LayoutAlignment::new(Origin, Origin),
|
|
actions: vec![]
|
|
}));
|
|
|
|
self.space.last_spacing = LastSpacing::Hard;
|
|
}
|
|
|
|
// A soft 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.space.last_spacing {
|
|
LastSpacing::None => true,
|
|
LastSpacing::Soft(_, prev) if level < prev => true,
|
|
_ => false,
|
|
};
|
|
|
|
if consumes {
|
|
self.space.last_spacing = LastSpacing::Soft(spacing, level);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 += (dimensions.x - extra.x).max(Size::ZERO);
|
|
size.y += (dimensions.y - extra.y).max(Size::ZERO);
|
|
|
|
extra.x.max_eq(dimensions.x);
|
|
extra.y = (extra.y - dimensions.y).max(Size::ZERO);
|
|
|
|
self.space.size = size.specialized(axes);
|
|
self.space.extra = extra.specialized(axes);
|
|
*self.space.usable.get_secondary_mut(axes) -= dimensions.y;
|
|
}
|
|
|
|
/// Update the rulers to account for the new layout. Returns true if a
|
|
/// space break is necessary.
|
|
fn update_rulers(&mut self, alignment: LayoutAlignment) -> bool {
|
|
let allowed = self.is_fitting_alignment(alignment);
|
|
if allowed {
|
|
*self.space.rulers.get_mut(self.ctx.axes.secondary, Origin)
|
|
= alignment.secondary;
|
|
}
|
|
allowed
|
|
}
|
|
|
|
/// Whether a layout with the given alignment can still be layouted in the
|
|
/// active space.
|
|
pub fn is_fitting_alignment(&mut self, alignment: LayoutAlignment) -> bool {
|
|
self.is_fitting_axis(self.ctx.axes.primary, alignment.primary)
|
|
&& self.is_fitting_axis(self.ctx.axes.secondary, alignment.secondary)
|
|
}
|
|
|
|
/// Whether the given alignment is still allowed according to the rulers.
|
|
fn is_fitting_axis(&mut self, direction: Direction, alignment: Alignment) -> bool {
|
|
alignment >= *self.space.rulers.get_mut(direction, Origin)
|
|
&& alignment <= self.space.rulers.get_mut(direction, End).inv()
|
|
}
|
|
|
|
/// Change the layouting axes used by this layouter.
|
|
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;
|
|
}
|
|
|
|
/// 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 dimensions = self.usable();
|
|
|
|
let mut spaces = smallvec![LayoutSpace {
|
|
dimensions,
|
|
padding: SizeBox::ZERO,
|
|
expansion: LayoutExpansion::new(false, false),
|
|
}];
|
|
|
|
for space in &self.ctx.spaces[self.next_space()..] {
|
|
spaces.push(space.usable_space());
|
|
}
|
|
|
|
spaces
|
|
}
|
|
|
|
/// The remaining usable size.
|
|
pub fn usable(&self) -> Size2D {
|
|
self.space.usable
|
|
- Size2D::with_y(self.space.last_spacing.soft_or_zero())
|
|
.specialized(self.ctx.axes)
|
|
}
|
|
|
|
/// Whether the current layout space (not subspace) is empty.
|
|
pub fn space_is_empty(&self) -> bool {
|
|
self.space.size == Size2D::ZERO && self.space.layouts.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) -> LayoutResult<MultiLayout> {
|
|
if self.space.hard || !self.space_is_empty() {
|
|
self.finish_space(false)?;
|
|
}
|
|
Ok(self.layouts)
|
|
}
|
|
|
|
/// Finish the current space and start a new one.
|
|
pub fn finish_space(&mut self, hard: bool) -> LayoutResult<()> {
|
|
if !self.ctx.repeat && hard {
|
|
error!("cannot create new space in a non-repeating context");
|
|
}
|
|
|
|
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();
|
|
if space.expansion.horizontal { self.space.size.x = usable.x; }
|
|
if space.expansion.vertical { self.space.size.y = usable.y; }
|
|
|
|
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.get_mut(axes.secondary, Origin)
|
|
+= axes.secondary.factor() * layout.dimensions.get_secondary(*axes);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// Step 3: Backward pass. Reduce the bounding boxes from the previous
|
|
// layouts by what is taken by the following ones.
|
|
|
|
// The `x` field stores the maximal primary extent in one axis-aligned
|
|
// run, while the `y` fields stores the accumulated secondary extent.
|
|
let mut extent = Size2D::ZERO;
|
|
let mut rotation = Vertical;
|
|
|
|
for (bound, entry) in bounds.iter_mut().zip(&self.space.layouts).rev() {
|
|
let (axes, layout) = entry;
|
|
|
|
// When the axes get rotated, the the maximal primary size
|
|
// (`extent.x`) dictates how much secondary extent the whole run
|
|
// had. This value is thus stored in `extent.y`. The primary extent
|
|
// is reset for this new axis-aligned run.
|
|
if rotation != axes.secondary.axis() {
|
|
extent.y = extent.x;
|
|
extent.x = Size::ZERO;
|
|
rotation = axes.secondary.axis();
|
|
}
|
|
|
|
// 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.get_mut(axes.secondary, End)
|
|
-= axes.secondary.factor() * extent.y;
|
|
|
|
// Then, we add this layout's secondary extent to the accumulator.
|
|
let size = layout.dimensions.generalized(*axes);
|
|
extent.x.max_eq(size.x);
|
|
extent.y += size.y;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// Step 4: Align each layout in its bounding box and collect everything
|
|
// into a single finished layout.
|
|
|
|
let mut actions = LayoutActions::new();
|
|
|
|
if self.ctx.debug {
|
|
actions.add(LayoutAction::DebugBox(dimensions));
|
|
}
|
|
|
|
let layouts = std::mem::replace(&mut self.space.layouts, vec![]);
|
|
for ((axes, layout), bound) in layouts.into_iter().zip(bounds) {
|
|
let size = layout.dimensions.specialized(axes);
|
|
let alignment = layout.alignment;
|
|
|
|
// The space in which this layout is aligned is given by the
|
|
// distances between the borders of it's bounding box.
|
|
let usable =
|
|
Size2D::new(bound.right - bound.left, bound.bottom - bound.top)
|
|
.generalized(axes);
|
|
|
|
let local = usable.anchor(alignment, axes) - size.anchor(alignment, axes);
|
|
let pos = Size2D::new(bound.left, bound.top) + local.specialized(axes);
|
|
|
|
actions.add_layout(pos, layout);
|
|
}
|
|
|
|
self.layouts.push(Layout {
|
|
dimensions,
|
|
alignment: self.ctx.alignment,
|
|
actions: actions.to_vec(),
|
|
});
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// Step 5: Start the next space.
|
|
|
|
Ok(self.start_space(self.next_space(), hard))
|
|
}
|
|
|
|
/// Start a new space with the given index.
|
|
fn start_space(&mut self, index: usize, hard: bool) {
|
|
let space = self.ctx.spaces[index];
|
|
self.space = Space::new(index, hard, space.usable());
|
|
}
|
|
|
|
/// The index of the next space.
|
|
fn next_space(&self) -> usize {
|
|
(self.space.index + 1).min(self.ctx.spaces.len() - 1)
|
|
}
|
|
}
|
|
|
|
impl Space {
|
|
fn new(index: usize, hard: bool, usable: Size2D) -> Space {
|
|
Space {
|
|
index,
|
|
hard,
|
|
layouts: vec![],
|
|
size: Size2D::ZERO,
|
|
usable,
|
|
extra: Size2D::ZERO,
|
|
rulers: ValueBox::with_all(Origin),
|
|
last_spacing: LastSpacing::Hard,
|
|
}
|
|
}
|
|
}
|