213 lines
6.2 KiB
Rust
213 lines
6.2 KiB
Rust
use super::*;
|
|
|
|
/// Layouts boxes flex-like.
|
|
///
|
|
/// The boxes are arranged in "lines", each line having the height of its
|
|
/// biggest box. When a box does not fit on a line anymore horizontally,
|
|
/// a new line is started.
|
|
///
|
|
/// The flex layouter does not actually compute anything until the `finish`
|
|
/// method is called. The reason for this is the flex layouter will have
|
|
/// the capability to justify its layouts, later. To find a good justification
|
|
/// it needs total information about the contents.
|
|
///
|
|
/// There are two different kinds units that can be added to a flex run:
|
|
/// Normal layouts and _glue_. _Glue_ layouts are only written if a normal
|
|
/// layout follows and a glue layout is omitted if the following layout
|
|
/// flows into a new line. A _glue_ layout is typically used for a space character
|
|
/// since it prevents a space from appearing in the beginning or end of a line.
|
|
/// However, it can be any layout.
|
|
pub struct FlexLayouter {
|
|
ctx: FlexContext,
|
|
units: Vec<FlexUnit>,
|
|
|
|
actions: LayoutActionList,
|
|
usable: Size2D,
|
|
dimensions: Size2D,
|
|
cursor: Size2D,
|
|
|
|
run: FlexRun,
|
|
next_glue: Option<Layout>,
|
|
}
|
|
|
|
/// The context for flex layouting.
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub struct FlexContext {
|
|
/// The space to layout the boxes in.
|
|
pub space: LayoutSpace,
|
|
/// The spacing between two lines of boxes.
|
|
pub flex_spacing: Size,
|
|
}
|
|
|
|
enum FlexUnit {
|
|
/// A content unit to be arranged flexibly.
|
|
Boxed(Layout),
|
|
/// A unit which acts as glue between two [`FlexUnit::Boxed`] units and
|
|
/// is only present if there was no flow break in between the two
|
|
/// surrounding boxes.
|
|
Glue(Layout),
|
|
}
|
|
|
|
struct FlexRun {
|
|
content: Vec<(Size2D, Layout)>,
|
|
size: Size2D,
|
|
}
|
|
|
|
impl FlexLayouter {
|
|
/// Create a new flex layouter.
|
|
pub fn new(ctx: FlexContext) -> FlexLayouter {
|
|
FlexLayouter {
|
|
ctx,
|
|
units: vec![],
|
|
|
|
actions: LayoutActionList::new(),
|
|
usable: ctx.space.usable(),
|
|
dimensions: match ctx.space.alignment {
|
|
Alignment::Left => Size2D::zero(),
|
|
Alignment::Right => Size2D::with_x(ctx.space.usable().x),
|
|
},
|
|
|
|
cursor: Size2D::new(ctx.space.padding.left, ctx.space.padding.top),
|
|
|
|
run: FlexRun::new(),
|
|
next_glue: None,
|
|
}
|
|
}
|
|
|
|
/// This layouter's context.
|
|
pub fn ctx(&self) -> FlexContext {
|
|
self.ctx
|
|
}
|
|
|
|
/// Add a sublayout.
|
|
pub fn add(&mut self, layout: Layout) {
|
|
self.units.push(FlexUnit::Boxed(layout));
|
|
}
|
|
|
|
/// Add a glue layout which can be replaced by a line break.
|
|
pub fn add_glue(&mut self, glue: Layout) {
|
|
self.units.push(FlexUnit::Glue(glue));
|
|
}
|
|
|
|
/// Compute the justified layout.
|
|
pub fn finish(mut self) -> LayoutResult<Layout> {
|
|
// Move the units out of the layout because otherwise, we run into
|
|
// ownership problems.
|
|
let units = self.units;
|
|
self.units = Vec::new();
|
|
|
|
for unit in units {
|
|
match unit {
|
|
FlexUnit::Boxed(boxed) => self.layout_box(boxed)?,
|
|
FlexUnit::Glue(glue) => self.layout_glue(glue),
|
|
}
|
|
}
|
|
|
|
// Finish the last flex run.
|
|
self.finish_flex_run();
|
|
|
|
Ok(Layout {
|
|
dimensions: if self.ctx.space.shrink_to_fit {
|
|
self.dimensions.padded(self.ctx.space.padding)
|
|
} else {
|
|
self.ctx.space.dimensions
|
|
},
|
|
actions: self.actions.into_vec(),
|
|
debug_render: true,
|
|
})
|
|
}
|
|
|
|
/// Whether this layouter contains any items.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.units.is_empty()
|
|
}
|
|
|
|
fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> {
|
|
let next_glue_width = self
|
|
.next_glue
|
|
.as_ref()
|
|
.map(|g| g.dimensions.x)
|
|
.unwrap_or(Size::zero());
|
|
|
|
let new_line_width = self.run.size.x + next_glue_width + boxed.dimensions.x;
|
|
|
|
if self.overflows(new_line_width) {
|
|
// If the box does not even fit on its own line, then
|
|
// we can't do anything.
|
|
if self.overflows(boxed.dimensions.x) {
|
|
return Err(LayoutError::NotEnoughSpace);
|
|
}
|
|
|
|
self.next_glue = None;
|
|
self.finish_flex_run();
|
|
} else {
|
|
// Only add the glue if we did not move to a new line.
|
|
self.flush_glue();
|
|
}
|
|
|
|
self.add_to_flex_run(boxed);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn layout_glue(&mut self, glue: Layout) {
|
|
self.flush_glue();
|
|
self.next_glue = Some(glue);
|
|
}
|
|
|
|
fn flush_glue(&mut self) {
|
|
if let Some(glue) = self.next_glue.take() {
|
|
self.add_to_flex_run(glue);
|
|
}
|
|
}
|
|
|
|
fn add_to_flex_run(&mut self, layout: Layout) {
|
|
let position = self.cursor;
|
|
|
|
self.cursor.x += layout.dimensions.x;
|
|
self.run.size.x += layout.dimensions.x;
|
|
self.run.size.y = crate::size::max(self.run.size.y, layout.dimensions.y);
|
|
|
|
self.run.content.push((position, layout));
|
|
}
|
|
|
|
fn finish_flex_run(&mut self) {
|
|
// Add all layouts from the current flex run at the correct positions.
|
|
match self.ctx.space.alignment {
|
|
Alignment::Left => {
|
|
for (position, layout) in self.run.content.drain(..) {
|
|
self.actions.add_layout(position, layout);
|
|
}
|
|
}
|
|
|
|
Alignment::Right => {
|
|
let extra_space = Size2D::with_x(self.usable.x - self.run.size.x);
|
|
for (position, layout) in self.run.content.drain(..) {
|
|
self.actions.add_layout(position + extra_space, layout);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.dimensions.x = crate::size::max(self.dimensions.x, self.run.size.x);
|
|
self.dimensions.y += self.ctx.flex_spacing;
|
|
self.dimensions.y += self.run.size.y;
|
|
|
|
self.cursor.x = self.ctx.space.padding.left;
|
|
self.cursor.y += self.run.size.y + self.ctx.flex_spacing;
|
|
self.run.size = Size2D::zero();
|
|
}
|
|
|
|
fn overflows(&self, line: Size) -> bool {
|
|
line > self.usable.x
|
|
}
|
|
}
|
|
|
|
impl FlexRun {
|
|
fn new() -> FlexRun {
|
|
FlexRun {
|
|
content: vec![],
|
|
size: Size2D::zero()
|
|
}
|
|
}
|
|
}
|