summaryrefslogtreecommitdiff
path: root/src/layout/line.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2019-12-30 22:28:56 +0100
committerLaurenz <laurmaedje@gmail.com>2019-12-30 22:28:56 +0100
commit269f069a4d721a986807293ef71be1348bfae3d4 (patch)
tree31b737c4ff2aead3eb5e2673828595bb26622032 /src/layout/line.rs
parentb8620121a692df6313eeb5ccf7baf89c1e364116 (diff)
Simple line layouter 🧾
Diffstat (limited to 'src/layout/line.rs')
-rw-r--r--src/layout/line.rs283
1 files changed, 283 insertions, 0 deletions
diff --git a/src/layout/line.rs b/src/layout/line.rs
new file mode 100644
index 00000000..5aae7dcd
--- /dev/null
+++ b/src/layout/line.rs
@@ -0,0 +1,283 @@
+use super::*;
+
+/// The line layouter arranges boxes next to each other along a primary axis
+/// and arranges the resulting lines using an underlying stack layouter.
+#[derive(Debug, Clone)]
+pub struct LineLayouter {
+ /// The context for layouting.
+ ctx: LineContext,
+ /// The underlying stack layouter.
+ stack: StackLayouter,
+ /// The currently written line.
+ run: LineRun,
+}
+
+/// The context for line layouting.
+#[derive(Debug, Clone)]
+pub struct LineContext {
+ /// The spaces to layout in.
+ pub spaces: LayoutSpaces,
+ /// The initial layouting axes, which can be updated by the
+ /// [`LineLayouter::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,
+ /// The line spacing.
+ pub line_spacing: Size,
+}
+
+/// A simple line of boxes.
+#[derive(Debug, Clone)]
+struct LineRun {
+ /// The so-far accumulated layouts in the line.
+ layouts: Vec<(Size, Layout)>,
+ /// The width (primary size) and maximal height (secondary size) of the
+ /// line.
+ size: Size2D,
+ /// The alignment of all layouts in the line.
+ alignment: Option<LayoutAlignment>,
+ /// The remaining usable space if another differently aligned line run
+ /// already took up some space.
+ usable: Option<Size>,
+ /// A possibly cached soft spacing or spacing state.
+ last_spacing: LastSpacing,
+}
+
+impl LineLayouter {
+ /// Create a new line layouter.
+ pub fn new(ctx: LineContext) -> LineLayouter {
+ LineLayouter {
+ stack: StackLayouter::new(StackContext {
+ spaces: ctx.spaces.clone(),
+ axes: ctx.axes,
+ alignment: ctx.alignment,
+ repeat: ctx.repeat,
+ debug: ctx.debug,
+ }),
+ ctx,
+ run: LineRun::new(),
+ }
+ }
+
+ /// Add a layout to the run.
+ pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
+ let axes = self.ctx.axes;
+
+ if let Some(alignment) = self.run.alignment {
+ if layout.alignment.secondary != alignment.secondary {
+ if self.stack.is_fitting_alignment(layout.alignment) {
+ self.finish_line()?;
+ } else {
+ self.finish_space(true)?;
+ }
+ } else if layout.alignment.primary < alignment.primary {
+ self.finish_line()?;
+
+ } else if layout.alignment.primary > alignment.primary {
+ let mut rest_run = LineRun::new();
+
+ let usable = self.stack.usable().get_primary(axes);
+ rest_run.usable = Some(match layout.alignment.primary {
+ Alignment::Origin => unreachable!("origin > x"),
+ Alignment::Center => usable - 2 * self.run.size.x,
+ Alignment::End => usable - self.run.size.x,
+ });
+
+ rest_run.size.y = self.run.size.y;
+
+ self.finish_line()?;
+ self.stack.add_spacing(-rest_run.size.y, SpacingKind::Hard);
+
+ self.run = rest_run;
+ }
+ }
+
+ if let LastSpacing::Soft(spacing, _) = self.run.last_spacing {
+ self.add_primary_spacing(spacing, SpacingKind::Hard);
+ }
+
+ let size = layout.dimensions.generalized(axes);
+
+ while !self.usable().fits(size) {
+ if !self.line_is_empty() {
+ self.finish_line()?;
+ } else {
+ self.finish_space(true)?;
+ }
+ }
+
+ self.run.alignment = Some(layout.alignment);
+ self.run.layouts.push((self.run.size.x, layout));
+
+ self.run.size.x += size.x;
+ self.run.size.y.max_eq(size.y);
+ self.run.last_spacing = LastSpacing::None;
+
+ Ok(())
+ }
+
+ /// Add multiple layouts to the run.
+ ///
+ /// 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(())
+ }
+
+ /// The remaining usable size in the run.
+ fn usable(&self) -> Size2D {
+ // The base is the usable space per stack layouter.
+ let mut usable = self.stack.usable().generalized(self.ctx.axes);
+
+ // If this is a alignment-continuing line, we override the primary
+ // usable size.
+ if let Some(primary) = self.run.usable {
+ usable.x = primary;
+ }
+
+ usable.x -= self.run.size.x;
+ usable
+ }
+
+ /// Add primary spacing to the line.
+ pub fn add_primary_spacing(&mut self, mut spacing: Size, kind: SpacingKind) {
+ match kind {
+ // A hard space is simply an empty box.
+ SpacingKind::Hard => {
+ spacing.min_eq(self.usable().x);
+ self.run.size.x += spacing;
+ self.run.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.run.last_spacing {
+ LastSpacing::None => true,
+ LastSpacing::Soft(_, prev) if level < prev => true,
+ _ => false,
+ };
+
+ if consumes {
+ self.run.last_spacing = LastSpacing::Soft(spacing, level);
+ }
+ }
+ }
+ }
+
+ /// Finish the run and add secondary spacing to the underlying stack.
+ pub fn add_secondary_spacing(
+ &mut self,
+ mut spacing: Size,
+ kind: SpacingKind
+ ) -> LayoutResult<()> {
+ self.finish_line_if_not_empty()?;
+ Ok(self.stack.add_spacing(spacing, kind))
+ }
+
+ /// Change the layouting axes used by this layouter.
+ pub fn set_axes(&mut self, axes: LayoutAxes) -> LayoutResult<()> {
+ self.finish_line_if_not_empty()?;
+ self.ctx.axes = axes;
+ Ok(self.stack.set_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) {
+ self.stack.set_spaces(spaces, replace_empty && self.line_is_empty());
+ }
+
+ /// Change the line spacing.
+ pub fn set_line_spacing(&mut self, line_spacing: Size) {
+ self.ctx.line_spacing = line_spacing;
+ }
+
+ /// The remaining unpadded, unexpanding spaces.
+ pub fn remaining(&self) -> LayoutSpaces {
+ let mut spaces = self.stack.remaining();
+ *spaces[0].dimensions.get_secondary_mut(self.ctx.axes)
+ -= self.run.size.y;
+ spaces
+ }
+
+ /// Whether the currently set line is empty.
+ pub fn line_is_empty(&self) -> bool {
+ self.run.size == Size2D::ZERO && self.run.layouts.is_empty()
+ }
+
+ /// Finish the last line and compute the final multi-layout.
+ pub fn finish(mut self) -> LayoutResult<MultiLayout> {
+ self.finish_line_if_not_empty()?;
+ self.stack.finish()
+ }
+
+ /// Finish the currently active space and start a new one.
+ pub fn finish_space(&mut self, hard: bool) -> LayoutResult<()> {
+ self.finish_line_if_not_empty()?;
+ self.stack.finish_space(hard)
+ }
+
+ /// Add the current line to the stack and start a new line.
+ pub fn finish_line(&mut self) -> LayoutResult<()> {
+ let mut actions = LayoutActions::new();
+
+ let layouts = std::mem::replace(&mut self.run.layouts, vec![]);
+ for (offset, layout) in layouts {
+ let x = match self.ctx.axes.primary.is_positive() {
+ true => offset,
+ false => self.run.size.x
+ - offset
+ - layout.dimensions.get_primary(self.ctx.axes),
+ };
+
+ let pos = Size2D::with_x(x);
+ actions.add_layout(pos, layout);
+ }
+
+ self.stack.add(Layout {
+ dimensions: self.run.size.specialized(self.ctx.axes),
+ alignment: self.run.alignment
+ .unwrap_or(LayoutAlignment::new(Origin, Origin)),
+ actions: actions.to_vec(),
+ })?;
+
+ self.run = LineRun::new();
+
+ self.stack.add_spacing(self.ctx.line_spacing, LINE_KIND);
+
+ Ok(())
+ }
+
+ /// Finish the current line if it is not empty.
+ fn finish_line_if_not_empty(&mut self) -> LayoutResult<()> {
+ if !self.line_is_empty() {
+ self.finish_line()
+ } else {
+ Ok(())
+ }
+ }
+}
+
+impl LineRun {
+ fn new() -> LineRun {
+ LineRun {
+ layouts: vec![],
+ size: Size2D::ZERO,
+ alignment: None,
+ usable: None,
+ last_spacing: LastSpacing::Hard,
+ }
+ }
+}