diff --git a/src/layout/actions.rs b/src/layout/actions.rs index bbefbfc02..67ad48b41 100644 --- a/src/layout/actions.rs +++ b/src/layout/actions.rs @@ -17,14 +17,13 @@ pub enum LayoutAction { /// Write text starting at the current position. WriteText(String), /// Visualize a box for debugging purposes. - /// Arguments are position and size. + /// The arguments are position and size. DebugBox(Size2D, Size2D), } impl LayoutAction { - /// Serialize this layout action into a string representation. + /// Serialize this layout action into an easy-to-parse string representation. pub fn serialize(&self, f: &mut W) -> io::Result<()> { - use LayoutAction::*; match self { MoveAbsolute(s) => write!(f, "m {:.4} {:.4}", s.x.to_pt(), s.y.to_pt()), SetFont(i, s) => write!(f, "f {} {}", i, s), @@ -55,7 +54,17 @@ impl Display for LayoutAction { debug_display!(LayoutAction); -/// Unifies and otimizes lists of actions. +/// A sequence of layouting actions. +/// +/// The sequence of actions is optimized as the actions are added. For example, +/// a font changing option will only be added if the selected font is not already active. +/// All configuration actions (like moving, setting fonts, ...) are only flushed when +/// content is written. +/// +/// Furthermore, the action list can translate absolute position into a coordinate system +/// with a different. This is realized in the `add_box` method, which allows a layout to +/// be added at a position, effectively translating all movement actions inside the layout +/// by the position. #[derive(Debug, Clone)] pub struct LayoutActionList { pub origin: Size2D, @@ -77,8 +86,7 @@ impl LayoutActionList { } } - /// Add an action to the list if it is not useless - /// (like changing to a font that is already active). + /// Add an action to the list. pub fn add(&mut self, action: LayoutAction) { match action { MoveAbsolute(pos) => self.next_pos = Some(self.origin + pos), @@ -89,16 +97,8 @@ impl LayoutActionList { } _ => { - if let Some(target) = self.next_pos.take() { - self.actions.push(MoveAbsolute(target)); - } - - if let Some((index, size)) = self.next_font.take() { - if (index, size) != self.active_font { - self.actions.push(SetFont(index, size)); - self.active_font = (index, size); - } - } + self.flush_position(); + self.flush_font(); self.actions.push(action); } @@ -113,20 +113,16 @@ impl LayoutActionList { } } - /// Add all actions from a box layout at a position. A move to the position - /// is generated and all moves inside the box layout are translated as - /// necessary. - pub fn add_box(&mut self, position: Size2D, layout: Layout) { - if let Some(target) = self.next_pos.take() { - self.actions.push(MoveAbsolute(target)); - } + /// Add a layout at a position. All move actions inside the layout are translated + /// by the position. + pub fn add_layout(&mut self, position: Size2D, layout: Layout) { + self.flush_position(); - self.next_pos = Some(position); self.origin = position; + self.next_pos = Some(position); if layout.debug_render { - self.actions - .push(LayoutAction::DebugBox(position, layout.dimensions)); + self.actions.push(DebugBox(position, layout.dimensions)); } self.extend(layout.actions); @@ -141,4 +137,21 @@ impl LayoutActionList { pub fn into_vec(self) -> Vec { self.actions } + + /// Append a cached move action if one is cached. + fn flush_position(&mut self) { + if let Some(target) = self.next_pos.take() { + self.actions.push(MoveAbsolute(target)); + } + } + + /// Append a cached font-setting action if one is cached. + fn flush_font(&mut self) { + if let Some((index, size)) = self.next_font.take() { + if (index, size) != self.active_font { + self.actions.push(SetFont(index, size)); + self.active_font = (index, size); + } + } + } } diff --git a/src/layout/flex.rs b/src/layout/flex.rs index ab1f066e3..33f3d38b1 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -1,19 +1,33 @@ use super::*; -/// Finishes a flex layout by justifying the positions of the individual boxes. -#[derive(Debug)] +/// Flex-layouting of boxes. +/// +/// 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, actions: LayoutActionList, - dimensions: Size2D, usable: Size2D, + dimensions: Size2D, cursor: Size2D, - line_content: Vec<(Size2D, Layout)>, - line_metrics: Size2D, - last_glue: Option, + run: FlexRun, + next_glue: Option, } /// The context for flex layouting. @@ -21,12 +35,10 @@ pub struct FlexLayouter { pub struct FlexContext { /// The space to layout the boxes in. pub space: LayoutSpace, - /// The flex spacing between two lines of boxes. + /// The spacing between two lines of boxes. pub flex_spacing: Size, } -/// A unit in a flex layout. -#[derive(Debug, Clone)] enum FlexUnit { /// A content unit to be arranged flexibly. Boxed(Layout), @@ -36,6 +48,11 @@ enum FlexUnit { Glue(Layout), } +struct FlexRun { + content: Vec<(Size2D, Layout)>, + size: Size2D, +} + impl FlexLayouter { /// Create a new flex layouter. pub fn new(ctx: FlexContext) -> FlexLayouter { @@ -44,16 +61,16 @@ impl FlexLayouter { 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), }, - usable: ctx.space.usable(), + cursor: Size2D::new(ctx.space.padding.left, ctx.space.padding.top), - line_content: vec![], - line_metrics: Size2D::zero(), - last_glue: None, + run: FlexRun::new(), + next_glue: None, } } @@ -72,27 +89,22 @@ impl FlexLayouter { self.units.push(FlexUnit::Glue(glue)); } - /// Whether this layouter contains any items. - pub fn is_empty(&self) -> bool { - self.units.is_empty() - } - /// Compute the justified layout. pub fn finish(mut self) -> LayoutResult { - // Move the units out of the layout. + // Move the units out of the layout because otherwise, we run into + // ownership problems. let units = self.units; - self.units = vec![]; + self.units = Vec::new(); - // Arrange the units. for unit in units { match unit { - FlexUnit::Boxed(boxed) => self.boxed(boxed)?, - FlexUnit::Glue(glue) => self.glue(glue), + FlexUnit::Boxed(boxed) => self.layout_box(boxed)?, + FlexUnit::Glue(glue) => self.layout_glue(glue), } } - // Flush everything to get the correct dimensions. - self.newline(); + // Finish the last flex run. + self.finish_flex_run(); Ok(Layout { dimensions: if self.ctx.space.shrink_to_fit { @@ -105,82 +117,95 @@ impl FlexLayouter { }) } - /// Layout the box. - fn boxed(&mut self, boxed: Layout) -> LayoutResult<()> { - let last_glue_x = self - .last_glue + /// 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()); - // Move to the next line if necessary. - if self.line_metrics.x + boxed.dimensions.x + last_glue_x > self.usable.x { - // If it still does not fit, we stand no chance. - if boxed.dimensions.x > self.usable.x { + 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.newline(); - } else if let Some(glue) = self.last_glue.take() { - self.append(glue); + self.finish_flex_run(); + } else { + // Only add the glue if we did not move to a new line. + self.flush_glue(); } - self.append(boxed); + self.add_to_flex_run(boxed); Ok(()) } - /// Layout the glue. - fn glue(&mut self, glue: Layout) { - if let Some(glue) = self.last_glue.take() { - self.append(glue); + 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); } - self.last_glue = Some(glue); } - /// Append a box to the layout without checking anything. - fn append(&mut self, layout: Layout) { - let dim = layout.dimensions; - self.line_content.push((self.cursor, layout)); + fn add_to_flex_run(&mut self, layout: Layout) { + let position = self.cursor; - self.line_metrics.x += dim.x; - self.line_metrics.y = crate::size::max(self.line_metrics.y, dim.y); - self.cursor.x += dim.x; + 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)); } - /// Move to the next line. - fn newline(&mut self) { - // Move all actions into this layout and translate absolute positions. - let remaining_space = Size2D::with_x(self.ctx.space.usable().x - self.line_metrics.x); - for (cursor, layout) in self.line_content.drain(..) { - let position = match self.ctx.space.alignment { - Alignment::Left => cursor, - Alignment::Right => { - // Right align everything by shifting it right by the - // amount of space left to the right of the line. - cursor + remaining_space + 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); } - }; + } - self.actions.add_box(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); + } + } } - // Stretch the dimensions to at least the line width. - self.dimensions.x = crate::size::max(self.dimensions.x, self.line_metrics.x); + 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; - // If we wrote a line previously add the inter-line spacing. - if self.dimensions.y > Size::zero() { - self.dimensions.y += self.ctx.flex_spacing; - } - - self.dimensions.y += self.line_metrics.y; - - // Reset the cursor the left and move down by the line and the inter-line - // spacing. self.cursor.x = self.ctx.space.padding.left; - self.cursor.y += self.line_metrics.y + self.ctx.flex_spacing; + self.cursor.y += self.run.size.y + self.ctx.flex_spacing; + self.run.size = Size2D::zero(); + } - // Reset the current line metrics. - self.line_metrics = Size2D::zero(); + fn overflows(&self, line: Size) -> bool { + line > self.usable.x + } +} + +impl FlexRun { + fn new() -> FlexRun { + FlexRun { + content: vec![], + size: Size2D::zero() + } } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index abf140d63..b760ca1e9 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -267,7 +267,7 @@ impl<'a, 'p> Layouter<'a, 'p> { let boxed = layout.finish()?; - self.stack_layouter.add_box(boxed) + self.stack_layouter.add(boxed) } /// Layout a function. @@ -287,7 +287,7 @@ impl<'a, 'p> Layouter<'a, 'p> { for command in commands { match command { Command::Layout(tree) => self.layout(tree)?, - Command::Add(layout) => self.stack_layouter.add_box(layout)?, + Command::Add(layout) => self.stack_layouter.add(layout)?, Command::AddMany(layouts) => self.stack_layouter.add_many(layouts)?, Command::ToggleStyleClass(class) => self.style.to_mut().toggle_class(class), } diff --git a/src/layout/stacked.rs b/src/layout/stacked.rs index 5ca32970b..3c9946a6a 100644 --- a/src/layout/stacked.rs +++ b/src/layout/stacked.rs @@ -1,34 +1,41 @@ use super::*; -/// Layouts boxes block-style. -#[derive(Debug)] +/// Stack-like layouting of boxes. +/// +/// The boxes are arranged vertically, each layout gettings it's own "line". pub struct StackLayouter { ctx: StackContext, actions: LayoutActionList, - dimensions: Size2D, usable: Size2D, + dimensions: Size2D, cursor: Size2D, } +/// The context for the [`StackLayouter`]. #[derive(Debug, Copy, Clone)] pub struct StackContext { pub space: LayoutSpace, } impl StackLayouter { - /// Create a new box layouter. + /// Create a new stack layouter. pub fn new(ctx: StackContext) -> StackLayouter { let space = ctx.space; StackLayouter { ctx, actions: LayoutActionList::new(), + + usable: ctx.space.usable(), dimensions: match ctx.space.alignment { Alignment::Left => Size2D::zero(), Alignment::Right => Size2D::with_x(space.usable().x), }, - usable: space.usable(), + cursor: Size2D::new( + // If left-align, the cursor points to the top-left corner of + // each box. If we right-align, it points to the top-right + // corner. match ctx.space.alignment { Alignment::Left => space.padding.left, Alignment::Right => space.dimensions.x - space.padding.right, @@ -43,22 +50,17 @@ impl StackLayouter { &self.ctx } - /// Add a sublayout. - pub fn add_box(&mut self, layout: Layout) -> LayoutResult<()> { - // In the flow direction (vertical) add the layout and in the second - // direction just consider the maximal size of any child layout. - let new_size = Size2D { + /// Add a sublayout to the bottom. + pub fn add(&mut self, layout: Layout) -> LayoutResult<()> { + let new_dimensions = Size2D { x: crate::size::max(self.dimensions.x, layout.dimensions.x), y: self.dimensions.y + layout.dimensions.y, }; - // Check whether this box fits. - if self.overflows(new_size) { + if self.overflows(new_dimensions) { return Err(LayoutError::NotEnoughSpace); } - self.dimensions = new_size; - // Determine where to put the box. When we right-align it, we want the // cursor to point to the top-right corner of the box. Therefore, the // position has to be moved to the left by the width of the box. @@ -68,28 +70,23 @@ impl StackLayouter { }; self.cursor.y += layout.dimensions.y; + self.dimensions = new_dimensions; - self.add_box_absolute(position, layout); + self.actions.add_layout(position, layout); Ok(()) } - /// Add multiple sublayouts. + /// Add multiple sublayouts from a multi-layout. pub fn add_many(&mut self, layouts: MultiLayout) -> LayoutResult<()> { for layout in layouts { - self.add_box(layout)?; + self.add(layout)?; } Ok(()) } - /// Add a sublayout at an absolute position. - pub fn add_box_absolute(&mut self, position: Size2D, layout: Layout) { - self.actions.add_box(position, layout); - } - - /// Add space in between two boxes. + /// Add vertical space after the last layout. pub fn add_space(&mut self, space: Size) -> LayoutResult<()> { - // Check whether this space fits. if self.overflows(self.dimensions + Size2D::with_y(space)) { return Err(LayoutError::NotEnoughSpace); } @@ -100,20 +97,7 @@ impl StackLayouter { Ok(()) } - /// The remaining space for new boxes. - pub fn remaining(&self) -> Size2D { - Size2D { - x: self.usable.x, - y: self.usable.y - self.dimensions.y, - } - } - - /// Whether this layouter contains any items. - pub fn is_empty(&self) -> bool { - self.actions.is_empty() - } - - /// Finish the layouting and create a box layout from this. + /// Finish the layouting. pub fn finish(self) -> Layout { Layout { dimensions: if self.ctx.space.shrink_to_fit { @@ -126,8 +110,20 @@ impl StackLayouter { } } - /// Whether the given box is bigger than what we can hold. + /// The remaining space for new layouts. + pub fn remaining(&self) -> Size2D { + Size2D { + x: self.usable.x, + y: self.usable.y - self.dimensions.y, + } + } + + /// Whether this layouter contains any items. + pub fn is_empty(&self) -> bool { + self.actions.is_empty() + } + fn overflows(&self, dimensions: Size2D) -> bool { - dimensions.x > self.usable.x || dimensions.y > self.usable.y + !self.usable.fits(dimensions) } } diff --git a/src/size.rs b/src/size.rs index e837a6398..c74394397 100644 --- a/src/size.rs +++ b/src/size.rs @@ -130,6 +130,13 @@ impl Size2D { y: self.y + padding.top + padding.bottom, } } + + /// Whether the given [`Size2D`] fits into this one, that is, + /// both coordinate values are smaller. + #[inline] + pub fn fits(&self, other: Size2D) -> bool { + self.x >= other.x && self.y >= other.y + } } impl SizeBox {