Typesafe conversions in stack & line layouters 🍮

This commit is contained in:
Laurenz 2020-10-06 18:27:00 +02:00
parent 985fe28166
commit 4252f959f7
6 changed files with 197 additions and 223 deletions

View File

@ -6,9 +6,9 @@ pub use kurbo::*;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::ops::*; use std::ops::*;
use crate::layout::primitive::{Dir, Gen2, GenAlign, Get, Side, SpecAxis}; use crate::layout::{Dir, Gen2, GenAlign, Get, Side, Spec2, SpecAxis, Switch};
macro_rules! impl_get_2d { macro_rules! impl_2d {
($t:ty, $x:ident, $y:ident) => { ($t:ty, $x:ident, $y:ident) => {
impl Get<SpecAxis> for $t { impl Get<SpecAxis> for $t {
type Component = f64; type Component = f64;
@ -27,12 +27,20 @@ macro_rules! impl_get_2d {
} }
} }
} }
impl Switch for $t {
type Other = Gen2<f64>;
fn switch(self, dirs: Gen2<Dir>) -> Self::Other {
Spec2::new(self.$x, self.$y).switch(dirs)
}
}
}; };
} }
impl_get_2d!(Point, x, y); impl_2d!(Point, x, y);
impl_get_2d!(Vec2, x, y); impl_2d!(Vec2, x, y);
impl_get_2d!(Size, width, height); impl_2d!(Size, width, height);
impl Get<Side> for Rect { impl Get<Side> for Rect {
type Component = f64; type Component = f64;
@ -60,61 +68,36 @@ impl Get<Side> for Rect {
/// ///
/// [sizes]: ../../kurbo/struct.Size.html /// [sizes]: ../../kurbo/struct.Size.html
pub trait SizeExt { pub trait SizeExt {
/// Returns the generalized version of a `Size` based on the current
/// directions.
///
/// In the generalized version:
/// - `x` describes the cross axis instead of the horizontal one.
/// - `y` describes the main axis instead of the vertical one.
fn generalized(self, dirs: Gen2<Dir>) -> Self;
/// Returns the specialized version of this generalized `Size` (inverse to
/// `generalized`).
fn specialized(self, dirs: Gen2<Dir>) -> Self;
/// Whether the given size fits into this one, that is, both coordinate /// Whether the given size fits into this one, that is, both coordinate
/// values are smaller or equal. /// values are smaller or equal.
fn fits(self, other: Self) -> bool; fn fits(self, other: Self) -> bool;
/// The anchor position for an object to be aligned in a container with this /// The anchor position for an object to be aligned in a container with this
/// size and the given directions. /// size and the given directions.
///
/// This assumes the size to be generalized such that `width` corresponds to
/// the cross and `height` to the main axis.
fn anchor(self, dirs: Gen2<Dir>, aligns: Gen2<GenAlign>) -> Point; fn anchor(self, dirs: Gen2<Dir>, aligns: Gen2<GenAlign>) -> Point;
} }
impl SizeExt for Size { impl SizeExt for Size {
fn generalized(self, dirs: Gen2<Dir>) -> Self {
match dirs.main.axis() {
SpecAxis::Horizontal => Self::new(self.height, self.width),
SpecAxis::Vertical => self,
}
}
fn specialized(self, dirs: Gen2<Dir>) -> Self {
// Even though generalized is its own inverse, we still have this second
// function, for clarity at the call-site.
self.generalized(dirs)
}
fn fits(self, other: Self) -> bool { fn fits(self, other: Self) -> bool {
self.width >= other.width && self.height >= other.height self.width >= other.width && self.height >= other.height
} }
fn anchor(self, dirs: Gen2<Dir>, aligns: Gen2<GenAlign>) -> Point { fn anchor(self, dirs: Gen2<Dir>, aligns: Gen2<GenAlign>) -> Point {
fn anchor(length: f64, dir: Dir, align: GenAlign) -> f64 { fn anchor(length: f64, dir: Dir, align: GenAlign) -> f64 {
match (dir.is_positive(), align) { match if dir.is_positive() { align } else { align.inv() } {
(true, GenAlign::Start) | (false, GenAlign::End) => 0.0, GenAlign::Start => 0.0,
(_, GenAlign::Center) => length / 2.0, GenAlign::Center => length / 2.0,
(true, GenAlign::End) | (false, GenAlign::Start) => length, GenAlign::End => length,
} }
} }
Point::new( let switched = self.switch(dirs);
anchor(self.width, dirs.cross, aligns.cross), let generic = Gen2::new(
anchor(self.height, dirs.main, aligns.main), anchor(switched.main, dirs.main, aligns.main),
) anchor(switched.cross, dirs.cross, aligns.cross),
);
generic.switch(dirs).to_point()
} }
} }

View File

@ -49,11 +49,12 @@ impl LineLayouter {
/// Add a layout. /// Add a layout.
pub fn add(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) { pub fn add(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
let dirs = self.ctx.dirs;
if let Some(prev) = self.run.aligns { if let Some(prev) = self.run.aligns {
if aligns.main != prev.main { if aligns.main != prev.main {
// TODO: Issue warning for non-fitting alignment in // TODO: Issue warning for non-fitting alignment in
// non-repeating context. // non-repeating context.
let fitting = self.stack.is_fitting_alignment(aligns); let fitting = aligns.main >= self.stack.space.allowed_align;
if !fitting && self.ctx.repeat { if !fitting && self.ctx.repeat {
self.finish_space(true); self.finish_space(true);
} else { } else {
@ -62,20 +63,20 @@ impl LineLayouter {
} else if aligns.cross < prev.cross { } else if aligns.cross < prev.cross {
self.finish_line(); self.finish_line();
} else if aligns.cross > prev.cross { } else if aligns.cross > prev.cross {
let mut rest_run = LineRun::new(); let usable = self.stack.usable().get(dirs.cross.axis());
let usable = self.stack.usable().get(self.ctx.dirs.cross.axis()); let mut rest_run = LineRun::new();
rest_run.size.main = self.run.size.main;
rest_run.usable = Some(match aligns.cross { rest_run.usable = Some(match aligns.cross {
GenAlign::Start => unreachable!("start > x"), GenAlign::Start => unreachable!("start > x"),
GenAlign::Center => usable - 2.0 * self.run.size.width, GenAlign::Center => usable - 2.0 * self.run.size.cross,
GenAlign::End => usable - self.run.size.width, GenAlign::End => usable - self.run.size.cross,
}); });
rest_run.size.height = self.run.size.height;
self.finish_line(); self.finish_line();
self.stack.add_spacing(-rest_run.size.height, SpacingKind::Hard);
// Move back up in the stack layouter.
self.stack.add_spacing(-rest_run.size.main, SpacingKind::Hard);
self.run = rest_run; self.run = rest_run;
} }
} }
@ -84,24 +85,26 @@ impl LineLayouter {
self.add_cross_spacing(spacing, SpacingKind::Hard); self.add_cross_spacing(spacing, SpacingKind::Hard);
} }
let size = layout.size.generalized(self.ctx.dirs); let size = layout.size.switch(dirs);
let usable = self.usable();
if !self.usable().fits(size) { if usable.main < size.main || usable.cross < size.cross {
if !self.line_is_empty() { if !self.line_is_empty() {
self.finish_line(); self.finish_line();
} }
// TODO: Issue warning about overflow if there is overflow. // TODO: Issue warning about overflow if there is overflow.
if !self.usable().fits(size) { let usable = self.usable();
if usable.main < size.main || usable.cross < size.cross {
self.stack.skip_to_fitting_space(layout.size); self.stack.skip_to_fitting_space(layout.size);
} }
} }
self.run.aligns = Some(aligns); self.run.aligns = Some(aligns);
self.run.layouts.push((self.run.size.width, layout)); self.run.layouts.push((self.run.size.cross, layout));
self.run.size.width += size.width; self.run.size.cross += size.cross;
self.run.size.height = self.run.size.height.max(size.height); self.run.size.main = self.run.size.main.max(size.main);
self.run.last_spacing = LastSpacing::None; self.run.last_spacing = LastSpacing::None;
} }
@ -109,16 +112,16 @@ impl LineLayouter {
/// ///
/// This specifies how much more would fit before a line break would be /// This specifies how much more would fit before a line break would be
/// needed. /// needed.
fn usable(&self) -> Size { fn usable(&self) -> Gen2<f64> {
// The base is the usable space of the stack layouter. // The base is the usable space of the stack layouter.
let mut usable = self.stack.usable().generalized(self.ctx.dirs); let mut usable = self.stack.usable().switch(self.ctx.dirs);
// If there was another run already, override the stack's size. // If there was another run already, override the stack's size.
if let Some(cross) = self.run.usable { if let Some(cross) = self.run.usable {
usable.width = cross; usable.cross = cross;
} }
usable.width -= self.run.size.width; usable.cross -= self.run.size.cross;
usable usable
} }
@ -132,8 +135,8 @@ impl LineLayouter {
pub fn add_cross_spacing(&mut self, mut spacing: f64, kind: SpacingKind) { pub fn add_cross_spacing(&mut self, mut spacing: f64, kind: SpacingKind) {
match kind { match kind {
SpacingKind::Hard => { SpacingKind::Hard => {
spacing = spacing.min(self.usable().width); spacing = spacing.min(self.usable().cross);
self.run.size.width += spacing; self.run.size.cross += spacing;
self.run.last_spacing = LastSpacing::Hard; self.run.last_spacing = LastSpacing::Hard;
} }
@ -171,13 +174,13 @@ impl LineLayouter {
/// it will fit into this layouter's underlying stack. /// it will fit into this layouter's underlying stack.
pub fn remaining(&self) -> Vec<LayoutSpace> { pub fn remaining(&self) -> Vec<LayoutSpace> {
let mut spaces = self.stack.remaining(); let mut spaces = self.stack.remaining();
*spaces[0].size.get_mut(self.ctx.dirs.main.axis()) -= self.run.size.height; *spaces[0].size.get_mut(self.ctx.dirs.main.axis()) -= self.run.size.main;
spaces spaces
} }
/// Whether the currently set line is empty. /// Whether the currently set line is empty.
pub fn line_is_empty(&self) -> bool { pub fn line_is_empty(&self) -> bool {
self.run.size == Size::ZERO && self.run.layouts.is_empty() self.run.size == Gen2::ZERO && self.run.layouts.is_empty()
} }
/// Finish everything up and return the final collection of boxes. /// Finish everything up and return the final collection of boxes.
@ -196,19 +199,20 @@ impl LineLayouter {
/// Finish the active line and start a new one. /// Finish the active line and start a new one.
pub fn finish_line(&mut self) { pub fn finish_line(&mut self) {
let mut layout = BoxLayout::new(self.run.size.specialized(self.ctx.dirs)); let dirs = self.ctx.dirs;
let aligns = self.run.aligns.unwrap_or_default();
let cross = self.ctx.dirs.cross;
let layouts = std::mem::take(&mut self.run.layouts); let mut layout = BoxLayout::new(self.run.size.switch(dirs).to_size());
for (offset, child) in layouts { let aligns = self.run.aligns.unwrap_or_default();
let mut pos = Point::ZERO;
*pos.get_mut(cross.axis()) = if cross.is_positive() { let children = std::mem::take(&mut self.run.layouts);
for (offset, child) in children {
let cross = if dirs.cross.is_positive() {
offset offset
} else { } else {
self.run.size.width - offset - child.size.get(cross.axis()) self.run.size.cross - offset - child.size.get(dirs.cross.axis())
}; };
let pos = Gen2::new(0.0, cross).switch(dirs).to_point();
layout.push_layout(pos, child); layout.push_layout(pos, child);
} }
@ -231,15 +235,15 @@ struct LineRun {
/// The so-far accumulated items of the run. /// The so-far accumulated items of the run.
layouts: Vec<(f64, BoxLayout)>, layouts: Vec<(f64, BoxLayout)>,
/// The summed width and maximal height of the run. /// The summed width and maximal height of the run.
size: Size, size: Gen2<f64>,
/// The alignment of all layouts in the line. /// The alignment of all layouts in the line.
/// ///
/// When a new run is created the alignment is yet to be determined and /// When a new run is created the alignment is yet to be determined and
/// `None` as such. Once a layout is added, its alignment decides the /// `None` as such. Once a layout is added, its alignment decides the
/// alignment for the whole run. /// alignment for the whole run.
aligns: Option<Gen2<GenAlign>>, aligns: Option<Gen2<GenAlign>>,
/// The amount of space left by another run on the same line or `None` if /// The amount of cross-space left by another run on the same line or `None`
/// this is the only run so far. /// if this is the only run so far.
usable: Option<f64>, usable: Option<f64>,
/// The spacing state. This influences how new spacing is handled, e.g. hard /// The spacing state. This influences how new spacing is handled, e.g. hard
/// spacing may override soft spacing. /// spacing may override soft spacing.
@ -250,7 +254,7 @@ impl LineRun {
fn new() -> Self { fn new() -> Self {
Self { Self {
layouts: vec![], layouts: vec![],
size: Size::ZERO, size: Gen2::ZERO,
aligns: None, aligns: None,
usable: None, usable: None,
last_spacing: LastSpacing::Hard, last_spacing: LastSpacing::Hard,

View File

@ -2,6 +2,8 @@
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use crate::geom::{Point, Size, Vec2};
/// Generic access to a structure's components. /// Generic access to a structure's components.
pub trait Get<Index> { pub trait Get<Index> {
/// The structure's component type. /// The structure's component type.
@ -14,36 +16,16 @@ pub trait Get<Index> {
fn get_mut(&mut self, index: Index) -> &mut Self::Component; fn get_mut(&mut self, index: Index) -> &mut Self::Component;
} }
/// Convert a type into its generic representation. /// Switch between the specific and generic representations of a type.
/// ///
/// The generic representation deals with main and cross axes while the specific /// The generic representation deals with main and cross axes while the specific
/// representation deals with horizontal and vertical axes. /// representation deals with horizontal and vertical axes.
/// pub trait Switch {
/// See also [`ToSpec`] for the inverse conversion. /// The type of the other version.
/// type Other;
/// [`ToSpec`]: trait.ToSpec.html
pub trait ToGen {
/// The generic version of this type.
type Output;
/// The generic version of this type based on the current directions. /// The other version of this type based on the current directions.
fn to_gen(self, dirs: Gen2<Dir>) -> Self::Output; fn switch(self, dirs: Gen2<Dir>) -> Self::Other;
}
/// Convert a type into its specific representation.
///
/// The specific representation deals with horizontal and vertical axes while
/// the generic representation deals with main and cross axes.
///
/// See also [`ToGen`] for the inverse conversion.
///
/// [`ToGen`]: trait.ToGen.html
pub trait ToSpec {
/// The specific version of this type.
type Output;
/// The specific version of this type based on the current directions.
fn to_spec(self, dirs: Gen2<Dir>) -> Self::Output;
} }
/// The four directions into which content can be laid out. /// The four directions into which content can be laid out.
@ -68,6 +50,26 @@ impl Dir {
} }
} }
/// The side this direction starts at.
pub fn start(self) -> Side {
match self {
Self::LTR => Side::Left,
Self::RTL => Side::Right,
Self::TTB => Side::Top,
Self::BTT => Side::Bottom,
}
}
/// The side this direction ends at.
pub fn end(self) -> Side {
match self {
Self::LTR => Side::Right,
Self::RTL => Side::Left,
Self::TTB => Side::Bottom,
Self::BTT => Side::Top,
}
}
/// Whether this direction points into the positive coordinate direction. /// Whether this direction points into the positive coordinate direction.
/// ///
/// The positive directions are left-to-right and top-to-bottom. /// The positive directions are left-to-right and top-to-bottom.
@ -86,23 +88,6 @@ impl Dir {
if self.is_positive() { 1.0 } else { -1.0 } if self.is_positive() { 1.0 } else { -1.0 }
} }
/// The side of this direction the alignment identifies.
///
/// `Center` alignment is treated the same as `Start` alignment.
pub fn side(self, align: GenAlign) -> Side {
let start = match self {
Self::LTR => Side::Left,
Self::RTL => Side::Right,
Self::TTB => Side::Top,
Self::BTT => Side::Bottom,
};
match align {
GenAlign::Start | GenAlign::Center => start,
GenAlign::End => start.inv(),
}
}
/// The inverse direction. /// The inverse direction.
pub fn inv(self) -> Self { pub fn inv(self) -> Self {
match self { match self {
@ -159,10 +144,10 @@ impl<T> Get<GenAxis> for Gen2<T> {
} }
} }
impl<T> ToSpec for Gen2<T> { impl<T> Switch for Gen2<T> {
type Output = Spec2<T>; type Other = Spec2<T>;
fn to_spec(self, dirs: Gen2<Dir>) -> Self::Output { fn switch(self, dirs: Gen2<Dir>) -> Self::Other {
match dirs.main.axis() { match dirs.main.axis() {
SpecAxis::Horizontal => Spec2::new(self.main, self.cross), SpecAxis::Horizontal => Spec2::new(self.main, self.cross),
SpecAxis::Vertical => Spec2::new(self.cross, self.main), SpecAxis::Vertical => Spec2::new(self.cross, self.main),
@ -170,6 +155,11 @@ impl<T> ToSpec for Gen2<T> {
} }
} }
impl Gen2<f64> {
/// The instance that has both components set to zero.
pub const ZERO: Self = Self { main: 0.0, cross: 0.0 };
}
/// A generic container with two components for the two specific axes. /// A generic container with two components for the two specific axes.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct Spec2<T> { pub struct Spec2<T> {
@ -204,10 +194,10 @@ impl<T> Get<SpecAxis> for Spec2<T> {
} }
} }
impl<T> ToGen for Spec2<T> { impl<T> Switch for Spec2<T> {
type Output = Gen2<T>; type Other = Gen2<T>;
fn to_gen(self, dirs: Gen2<Dir>) -> Self::Output { fn switch(self, dirs: Gen2<Dir>) -> Self::Other {
match dirs.main.axis() { match dirs.main.axis() {
SpecAxis::Horizontal => Gen2::new(self.horizontal, self.vertical), SpecAxis::Horizontal => Gen2::new(self.horizontal, self.vertical),
SpecAxis::Vertical => Gen2::new(self.vertical, self.horizontal), SpecAxis::Vertical => Gen2::new(self.vertical, self.horizontal),
@ -215,6 +205,26 @@ impl<T> ToGen for Spec2<T> {
} }
} }
impl Spec2<f64> {
/// The instance that has both components set to zero.
pub const ZERO: Self = Self { horizontal: 0.0, vertical: 0.0 };
/// Convert to a 2D vector.
pub fn to_vec2(self) -> Vec2 {
Vec2::new(self.horizontal, self.vertical)
}
/// Convert to a point.
pub fn to_point(self) -> Point {
Point::new(self.horizontal, self.vertical)
}
/// Convert to a size.
pub fn to_size(self) -> Size {
Size::new(self.horizontal, self.vertical)
}
}
/// The two generic layouting axes. /// The two generic layouting axes.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum GenAxis { pub enum GenAxis {
@ -234,10 +244,10 @@ impl GenAxis {
} }
} }
impl ToSpec for GenAxis { impl Switch for GenAxis {
type Output = SpecAxis; type Other = SpecAxis;
fn to_spec(self, dirs: Gen2<Dir>) -> Self::Output { fn switch(self, dirs: Gen2<Dir>) -> Self::Other {
match self { match self {
Self::Main => dirs.main.axis(), Self::Main => dirs.main.axis(),
Self::Cross => dirs.cross.axis(), Self::Cross => dirs.cross.axis(),
@ -273,10 +283,10 @@ impl SpecAxis {
} }
} }
impl ToGen for SpecAxis { impl Switch for SpecAxis {
type Output = GenAxis; type Other = GenAxis;
fn to_gen(self, dirs: Gen2<Dir>) -> Self::Output { fn switch(self, dirs: Gen2<Dir>) -> Self::Other {
if self == dirs.main.axis() { if self == dirs.main.axis() {
GenAxis::Main GenAxis::Main
} else { } else {
@ -366,11 +376,10 @@ impl SpecAlign {
} }
} }
impl ToGen for SpecAlign { impl Switch for SpecAlign {
type Output = GenAlign; type Other = GenAlign;
fn to_gen(self, dirs: Gen2<Dir>) -> Self::Output { fn switch(self, dirs: Gen2<Dir>) -> Self::Other {
let dirs = dirs.to_spec(dirs);
let get = |dir: Dir, at_positive_start| { let get = |dir: Dir, at_positive_start| {
if dir.is_positive() == at_positive_start { if dir.is_positive() == at_positive_start {
GenAlign::Start GenAlign::Start
@ -379,6 +388,7 @@ impl ToGen for SpecAlign {
} }
}; };
let dirs = dirs.switch(dirs);
match self { match self {
Self::Left => get(dirs.horizontal, true), Self::Left => get(dirs.horizontal, true),
Self::Right => get(dirs.horizontal, false), Self::Right => get(dirs.horizontal, false),

View File

@ -28,7 +28,7 @@ pub struct StackLayouter {
/// The finished layouts. /// The finished layouts.
layouts: Vec<BoxLayout>, layouts: Vec<BoxLayout>,
/// The in-progress space. /// The in-progress space.
space: Space, pub(super) space: Space,
} }
/// The context for stack layouting. /// The context for stack layouting.
@ -57,30 +57,31 @@ impl StackLayouter {
/// Add a layout to the stack. /// Add a layout to the stack.
pub fn add(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) { pub fn add(&mut self, layout: BoxLayout, aligns: Gen2<GenAlign>) {
// If the alignment cannot be fitted in this space, finish it. // If the alignment cannot be fitted in this space, finish it.
//
// TODO: Issue warning for non-fitting alignment in non-repeating // TODO: Issue warning for non-fitting alignment in non-repeating
// context. // context.
if !self.update_rulers(aligns) && self.ctx.repeat { if aligns.main < self.space.allowed_align && self.ctx.repeat {
self.finish_space(true); self.finish_space(true);
} }
// Now, we add a possibly cached soft space. If the main alignment // Add a possibly cached soft spacing.
// changed before, a possibly cached space would have already been
// discarded.
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);
} }
// TODO: Issue warning about overflow if there is overflow. // TODO: Issue warning about overflow if there is overflow in a
// non-repeating context.
if !self.space.usable.fits(layout.size) && self.ctx.repeat { if !self.space.usable.fits(layout.size) && self.ctx.repeat {
self.skip_to_fitting_space(layout.size); self.skip_to_fitting_space(layout.size);
} }
// Change the usable space and size of the space. // Change the usable space and size of the space.
self.update_metrics(layout.size.generalized(self.ctx.dirs)); self.update_metrics(layout.size.switch(self.ctx.dirs));
// Add the box to the vector and remember that spacings are allowed // Add the box to the vector and remember that spacings are allowed
// again. // again.
self.space.layouts.push((layout, aligns)); self.space.layouts.push((layout, aligns));
self.space.allowed_align = aligns.main;
self.space.last_spacing = LastSpacing::None; self.space.last_spacing = LastSpacing::None;
} }
@ -93,10 +94,10 @@ impl StackLayouter {
let axis = self.ctx.dirs.main.axis(); let axis = self.ctx.dirs.main.axis();
spacing = spacing.min(self.space.usable.get(axis)); spacing = spacing.min(self.space.usable.get(axis));
let size = Size::new(0.0, spacing); let size = Gen2::new(spacing, 0.0);
self.update_metrics(size); self.update_metrics(size);
self.space.layouts.push(( self.space.layouts.push((
BoxLayout::new(size.specialized(self.ctx.dirs)), BoxLayout::new(size.switch(self.ctx.dirs).to_size()),
Gen2::default(), Gen2::default(),
)); ));
@ -119,40 +120,18 @@ impl StackLayouter {
} }
} }
fn update_metrics(&mut self, added: Size) { fn update_metrics(&mut self, added: Gen2<f64>) {
let mut size = self.space.size.generalized(self.ctx.dirs); let mut size = self.space.size.switch(self.ctx.dirs);
let mut extra = self.space.extra.generalized(self.ctx.dirs); let mut extra = self.space.extra.switch(self.ctx.dirs);
size.width += (added.width - extra.width).max(0.0); size.cross += (added.cross - extra.cross).max(0.0);
size.height += (added.height - extra.height).max(0.0); size.main += (added.main - extra.main).max(0.0);
extra.width = extra.width.max(added.width); extra.cross = extra.cross.max(added.cross);
extra.height = (extra.height - added.height).max(0.0); extra.main = (extra.main - added.main).max(0.0);
self.space.size = size.specialized(self.ctx.dirs); self.space.size = size.switch(self.ctx.dirs).to_size();
self.space.extra = extra.specialized(self.ctx.dirs); self.space.extra = extra.switch(self.ctx.dirs).to_size();
*self.space.usable.get_mut(self.ctx.dirs.main.axis()) -= added.height; *self.space.usable.get_mut(self.ctx.dirs.main.axis()) -= added.main;
}
/// Returns true if a space break is necessary.
fn update_rulers(&mut self, aligns: Gen2<GenAlign>) -> bool {
let allowed = self.is_fitting_alignment(aligns);
if allowed {
let side = self.ctx.dirs.main.side(GenAlign::Start);
*self.space.rulers.get_mut(side) = aligns.main;
}
allowed
}
/// Whether a layout with the given alignment can still be layouted into the
/// active space or a space break is necessary.
pub(crate) fn is_fitting_alignment(&self, aligns: Gen2<GenAlign>) -> bool {
self.is_fitting_axis(self.ctx.dirs.main, aligns.main)
&& self.is_fitting_axis(self.ctx.dirs.cross, aligns.cross)
}
fn is_fitting_axis(&self, dir: Dir, align: GenAlign) -> bool {
align >= self.space.rulers.get(dir.side(GenAlign::Start))
&& align <= self.space.rulers.get(dir.side(GenAlign::End)).inv()
} }
/// Update the layouting spaces. /// Update the layouting spaces.
@ -202,8 +181,9 @@ impl StackLayouter {
/// The remaining usable size. /// The remaining usable size.
pub fn usable(&self) -> Size { pub fn usable(&self) -> Size {
self.space.usable self.space.usable
- Size::new(0.0, self.space.last_spacing.soft_or_zero()) - Gen2::new(self.space.last_spacing.soft_or_zero(), 0.0)
.specialized(self.ctx.dirs) .switch(self.ctx.dirs)
.to_size()
} }
/// Whether the current layout space is empty. /// Whether the current layout space is empty.
@ -227,30 +207,36 @@ impl StackLayouter {
/// Finish active current space and start a new one. /// Finish active current space and start a new one.
pub fn finish_space(&mut self, hard: bool) { pub fn finish_space(&mut self, hard: bool) {
let dirs = self.ctx.dirs; let dirs = self.ctx.dirs;
let space = self.ctx.spaces[self.space.index];
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
// Step 1: Determine the full size of the space. // Step 1: Determine the full size of the space.
// (Mostly done already while collecting the boxes, but here we // (Mostly done already while collecting the boxes, but here we
// expand if necessary.) // expand if necessary.)
let space = self.ctx.spaces[self.space.index];
let start = space.start();
let padded_size = {
let mut used_size = self.space.size;
let usable = space.usable(); let usable = space.usable();
if space.expansion.horizontal { if space.expansion.horizontal {
self.space.size.width = usable.width; used_size.width = usable.width;
} }
if space.expansion.vertical { if space.expansion.vertical {
self.space.size.height = usable.height; used_size.height = usable.height;
} }
let size = self.space.size - space.insets.size(); used_size
};
let unpadded_size = padded_size - space.insets.size();
let mut layout = BoxLayout::new(unpadded_size);
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
// Step 2: Forward pass. Create a bounding box for each layout in which // 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 // it will be aligned. Then, go forwards through the boxes and remove
// what is taken by previous layouts from the following layouts. // what is taken by previous layouts from the following layouts.
let start = space.start();
let mut bounds = vec![]; let mut bounds = vec![];
let mut bound = Rect { let mut bound = Rect {
x0: start.x, x0: start.x,
@ -260,16 +246,16 @@ impl StackLayouter {
}; };
for (layout, _) in &self.space.layouts { for (layout, _) in &self.space.layouts {
// First, we store the bounds calculated so far (which were reduced // First, store the bounds calculated so far (which were reduced
// by the predecessors of this layout) as the initial bounding box // by the predecessors of this layout) as the initial bounding box
// of this layout. // of this layout.
bounds.push(bound); bounds.push(bound);
// Then, we reduce the bounding box for the following layouts. This // Then, reduce the bounding box for the following layouts. This
// layout uses up space from the origin to the end. Thus, it reduces // layout uses up space from the origin to the end. Thus, it reduces
// the usable space for following layouts at its origin by its // the usable space for following layouts at its origin by its
// main-axis extent. // main-axis extent.
*bound.get_mut(dirs.main.side(GenAlign::Start)) += *bound.get_mut(dirs.main.start()) +=
dirs.main.factor() * layout.size.get(dirs.main.axis()); dirs.main.factor() * layout.size.get(dirs.main.axis());
} }
@ -277,37 +263,30 @@ impl StackLayouter {
// Step 3: Backward pass. Reduce the bounding boxes from the previous // Step 3: Backward pass. Reduce the bounding boxes from the previous
// layouts by what is taken by the following ones. // layouts by what is taken by the following ones.
// The `x` field stores the maximal cross-axis extent in one
// axis-aligned run, while the `y` fields stores the accumulated
// main-axis extent.
let mut main_extent = 0.0; let mut main_extent = 0.0;
for (child, bound) in self.space.layouts.iter().zip(&mut bounds).rev() { for (child, bound) in self.space.layouts.iter().zip(&mut bounds).rev() {
// We reduce the bounding box of this layout at its end by the let (layout, _) = child;
// accumulated main-axis extent of all layouts we have seen so far
// (which are the layouts after this one since we iterate reversed).
*bound.get_mut(dirs.main.side(GenAlign::End)) -=
dirs.main.factor() * main_extent;
// And then, we include this layout's main-axis extent. // Reduce the bounding box of this layout by the following one's
main_extent += child.0.size.get(dirs.main.axis()); // main-axis extents.
*bound.get_mut(dirs.main.end()) -= dirs.main.factor() * main_extent;
// And then, include this layout's main-axis extent.
main_extent += layout.size.get(dirs.main.axis());
} }
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
// Step 4: Align each layout in its bounding box and collect everything // Step 4: Align each layout in its bounding box and collect everything
// into a single finished layout. // into a single finished layout.
let mut layout = BoxLayout::new(size);
let children = std::mem::take(&mut self.space.layouts); let children = std::mem::take(&mut self.space.layouts);
for ((child, aligns), bound) in children.into_iter().zip(bounds) { for ((child, aligns), bound) in children.into_iter().zip(bounds) {
let size = child.size.specialized(dirs); // Align the child in its own bounds.
let local =
// The space in which this layout is aligned is given by the bound.size().anchor(dirs, aligns) - child.size.anchor(dirs, aligns);
// distances between the borders of its bounding box.
let usable = bound.size().generalized(dirs);
let local = usable.anchor(dirs, aligns) - size.anchor(dirs, aligns);
let pos = bound.origin() + local.to_size().specialized(dirs).to_vec2();
// Make the local position in the bounds global.
let pos = bound.origin() + local;
layout.push_layout(pos, child); layout.push_layout(pos, child);
} }
@ -331,23 +310,21 @@ impl StackLayouter {
/// A layout space composed of subspaces which can have different directions and /// A layout space composed of subspaces which can have different directions and
/// alignments. /// alignments.
struct Space { pub(super) struct Space {
/// The index of this space in `ctx.spaces`. /// The index of this space in `ctx.spaces`.
index: usize, index: usize,
/// Whether to include a layout for this space even if it would be empty. /// Whether to include a layout for this space even if it would be empty.
hard: bool, hard: bool,
/// The so-far accumulated layouts. /// The so-far accumulated layouts.
layouts: Vec<(BoxLayout, Gen2<GenAlign>)>, layouts: Vec<(BoxLayout, Gen2<GenAlign>)>,
/// The specialized size of this space. /// The size of this space.
size: Size, size: Size,
/// The specialized remaining space. /// The remaining space.
usable: Size, usable: Size,
/// The specialized extra-needed size to affect the size at all. /// The extra-needed size to affect the size at all.
extra: Size, extra: Size,
/// Dictate which alignments for new boxes are still allowed and which /// Which alignments for new boxes are still allowed.
/// require a new space to be started. For example, after an `End`-aligned pub(super) allowed_align: GenAlign,
/// item, no `Start`-aligned one can follow.
rulers: Sides<GenAlign>,
/// The spacing state. This influences how new spacing is handled, e.g. hard /// The spacing state. This influences how new spacing is handled, e.g. hard
/// spacing may override soft spacing. /// spacing may override soft spacing.
last_spacing: LastSpacing, last_spacing: LastSpacing,
@ -362,7 +339,7 @@ impl Space {
size: Size::ZERO, size: Size::ZERO,
usable, usable,
extra: Size::ZERO, extra: Size::ZERO,
rulers: Sides::uniform(GenAlign::Start), allowed_align: GenAlign::Start,
last_spacing: LastSpacing::Hard, last_spacing: LastSpacing::Hard,
} }
} }

View File

@ -54,8 +54,8 @@ fn dedup_aligns(
// Check whether we know which axis this alignment belongs to. // Check whether we know which axis this alignment belongs to.
if let Some(axis) = axis { if let Some(axis) = axis {
// We know the axis. // We know the axis.
let gen_axis = axis.to_gen(ctx.state.dirs); let gen_axis = axis.switch(ctx.state.dirs);
let gen_align = align.to_gen(ctx.state.dirs); let gen_align = align.switch(ctx.state.dirs);
if align.axis().map_or(false, |a| a != axis) { if align.axis().map_or(false, |a| a != axis) {
ctx.diag(error!( ctx.diag(error!(

View File

@ -24,7 +24,7 @@ fn spacing(mut args: Args, ctx: &mut LayoutContext, axis: SpecAxis) -> Value {
Value::Commands(if let Some(spacing) = spacing { Value::Commands(if let Some(spacing) = spacing {
let spacing = spacing.eval(ctx.state.text.font_size()); let spacing = spacing.eval(ctx.state.text.font_size());
let axis = axis.to_gen(ctx.state.dirs); let axis = axis.switch(ctx.state.dirs);
vec![AddSpacing(spacing, SpacingKind::Hard, axis)] vec![AddSpacing(spacing, SpacingKind::Hard, axis)]
} else { } else {
vec![] vec![]