diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 6be0a3c8..7e9278ae 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -10,7 +10,7 @@ use image::codecs::png::PngEncoder; use image::{self, Pixel, Rgba, RgbaImage}; use parley::layout::{Alignment, Glyph, GlyphRun, Layout, PositionedLayoutItem}; use parley::style::{FontStack, FontWeight, StyleProperty, TextStyle}; -use parley::{AlignmentOptions, FontContext, InlineBox, LayoutContext, LineHeight}; +use parley::{AlignmentOptions, FontContext, InlineBox, InlineBoxKind, LayoutContext, LineHeight}; use std::fs::File; use swash::FontRef; use swash::scale::image::Content; @@ -94,6 +94,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 0, width: 50.0, height: 50.0, @@ -103,6 +104,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, index: 50, width: 50.0, height: 30.0, @@ -152,12 +154,14 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 40, width: 50.0, height: 50.0, }); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, index: 50, width: 50.0, height: 30.0, @@ -171,7 +175,7 @@ fn main() { // Perform layout (including bidi resolution and shaping) with start alignment layout.break_all_lines(max_advance); - layout.align(max_advance, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); // Create image to render into let width = layout.width().ceil() as u32 + (padding * 2); diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs index 5ad62933..1c2bbc0a 100644 --- a/examples/tiny_skia_render/src/main.rs +++ b/examples/tiny_skia_render/src/main.rs @@ -11,7 +11,7 @@ use parley::{ Alignment, AlignmentOptions, FontContext, FontWeight, GenericFamily, GlyphRun, InlineBox, - Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, + InlineBoxKind, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, }; use skrifa::{ GlyphId, MetadataProvider, OutlineGlyph, @@ -88,6 +88,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 40, width: 50.0, height: 50.0, @@ -98,7 +99,7 @@ fn main() { // Perform layout (including bidi resolution and shaping) with start alignment layout.break_all_lines(max_advance); - layout.align(max_advance, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); let width = layout.width().ceil() as u32; let height = layout.height().ceil() as u32; let padded_width = width + padding * 2; diff --git a/parley/src/builder.rs b/parley/src/builder.rs index c59bedf0..b33f8825 100644 --- a/parley/src/builder.rs +++ b/parley/src/builder.rs @@ -12,6 +12,7 @@ use super::layout::Layout; use alloc::string::String; use core::ops::RangeBounds; +use crate::InlineBoxKind; use crate::inline_box::InlineBox; use crate::resolve::tree::ItemKind; @@ -112,11 +113,14 @@ impl TreeBuilder<'_, B> { } pub fn push_inline_box(&mut self, mut inline_box: InlineBox) { - self.lcx.tree_style_builder.push_uncommitted_text(false); - self.lcx.tree_style_builder.set_is_span_first(false); - self.lcx - .tree_style_builder - .set_last_item_kind(ItemKind::InlineBox); + if inline_box.kind == InlineBoxKind::InFlow { + self.lcx.tree_style_builder.push_uncommitted_text(false); + self.lcx.tree_style_builder.set_is_span_first(false); + self.lcx + .tree_style_builder + .set_last_item_kind(ItemKind::InlineBox); + } + // TODO: arrange type better here to factor out the index inline_box.index = self.lcx.tree_style_builder.current_text_len(); self.lcx.inline_boxes.push(inline_box); diff --git a/parley/src/editing/cursor.rs b/parley/src/editing/cursor.rs index f8a422f3..1f90389f 100644 --- a/parley/src/editing/cursor.rs +++ b/parley/src/editing/cursor.rs @@ -443,9 +443,9 @@ fn cursor_rect(cluster: &Cluster<'_, B>, at_end: bool, size: f32) -> B let metrics = line.metrics(); BoundingBox::new( line_x as f64, - metrics.min_coord as f64, + metrics.block_min_coord as f64, (line_x + size) as f64, - metrics.max_coord as f64, + metrics.block_max_coord as f64, ) } @@ -454,9 +454,9 @@ fn last_line_cursor_rect(layout: &Layout, size: f32) -> BoundingBox let metrics = line.metrics(); BoundingBox::new( 0.0, - metrics.min_coord as f64, + metrics.block_min_coord as f64, size as f64, - metrics.max_coord as f64, + metrics.block_max_coord as f64, ) } else { BoundingBox::default() diff --git a/parley/src/editing/editor.rs b/parley/src/editing/editor.rs index d8c494d2..210ccc28 100644 --- a/parley/src/editing/editor.rs +++ b/parley/src/editing/editor.rs @@ -1212,7 +1212,7 @@ where self.layout = builder.build(&self.buffer); self.layout.break_all_lines(self.width); self.layout - .align(self.width, self.alignment, AlignmentOptions::default()); + .align(self.alignment, AlignmentOptions::default()); self.selection = self.selection.refresh(&self.layout); self.layout_dirty = false; self.generation.nudge(); diff --git a/parley/src/editing/selection.rs b/parley/src/editing/selection.rs index b9ac2f28..cca4aeb6 100644 --- a/parley/src/editing/selection.rs +++ b/parley/src/editing/selection.rs @@ -294,7 +294,7 @@ impl Selection { let h_pos = self .h_pos .unwrap_or_else(|| self.focus.geometry(layout, 0.0).x0 as f32); - let y = line.metrics().max_coord - line.metrics().ascent * 0.5; + let y = line.metrics().block_max_coord - line.metrics().ascent * 0.5; let new_focus = Cursor::from_point(layout, h_pos, y); let h_pos = Some(h_pos); if extend { @@ -531,8 +531,8 @@ impl Selection { continue; }; let metrics = line.metrics(); - let line_min = metrics.min_coord as f64; - let line_max = metrics.max_coord as f64; + let line_min = metrics.block_min_coord as f64; + let line_max = metrics.block_max_coord as f64; // Trailing whitespace to indicate that the newline character at the // end of this line is selected. It's based on the ascent and // descent so it doesn't change with the line height. @@ -547,7 +547,7 @@ impl Selection { if line_ix == line_start_ix || line_ix == line_end_ix { // We only need to run the expensive logic on the first and // last lines - let mut start_x = metrics.offset as f64; + let mut start_x = metrics.offset as f64 + metrics.inline_min_coord as f64; let mut cur_x = start_x; let mut cluster_count = 0; let mut box_advance = 0.0; @@ -598,7 +598,7 @@ impl Selection { ); } } else { - let x = metrics.offset as f64; + let x = metrics.offset as f64 + metrics.inline_min_coord as f64; let width = metrics.advance as f64; f( BoundingBox::new(x, line_min, x + width + newline_whitespace, line_max), diff --git a/parley/src/inline_box.rs b/parley/src/inline_box.rs index 16a7a87b..e7324bda 100644 --- a/parley/src/inline_box.rs +++ b/parley/src/inline_box.rs @@ -7,6 +7,8 @@ pub struct InlineBox { /// User-specified identifier for the box, which can be used by the user to determine which box in /// parley's output corresponds to which box in its input. pub id: u64, + /// Whether the box is in-flow (takes up space in the layout) or out-of-flow (e.g. absolutely positioned or floated) + pub kind: InlineBoxKind, /// The byte offset into the underlying text string at which the box should be placed. /// This must not be within a Unicode code point. pub index: usize, @@ -15,3 +17,24 @@ pub struct InlineBox { /// The height of the box in pixels pub height: f32, } + +/// Whether a box is in-flow (takes up space in the layout) or out-of-flow (e.g. absolutely positioned) +/// or custom-out-of-flow (line-breaking should yield control flow) +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum InlineBoxKind { + /// `InFlow` boxes take up space in the layout and flow in line with text + /// + /// They correspond to `display: inline-block` boxes in CSS. + InFlow, + /// `OutOfFlow` boxes are assigned a position as if they were a zero-sized inline box, but + /// do not take up space in the layout. + /// + /// They correspond to `position: absolute` boxes in CSS. + OutOfFlow, + /// `CustomOutOfFlow` boxes also do not take up space in the layout, but they are not assigned a position + /// by Parley. When they are encountered, control flow is yielded back to the caller who is then responsible + /// for laying out the box. + /// + /// They can be used to implement advanced layout modes such as CSS's `float` + CustomOutOfFlow, +} diff --git a/parley/src/layout/accessibility.rs b/parley/src/layout/accessibility.rs index 9cb9e120..6ec4589a 100644 --- a/parley/src/layout/accessibility.rs +++ b/parley/src/layout/accessibility.rs @@ -87,9 +87,9 @@ impl LayoutAccessibility { node.set_bounds(accesskit::Rect { x0: x_offset + run_offset as f64, - y0: y_offset + metrics.min_coord as f64, + y0: y_offset + metrics.block_min_coord as f64, x1: x_offset + (run_offset + run.advance()) as f64, - y1: y_offset + metrics.max_coord as f64, + y1: y_offset + metrics.block_max_coord as f64, }); node.set_text_direction(if run.is_rtl() { TextDirection::RightToLeft diff --git a/parley/src/layout/alignment.rs b/parley/src/layout/alignment.rs index a0703bfb..039f9078 100644 --- a/parley/src/layout/alignment.rs +++ b/parley/src/layout/alignment.rs @@ -60,11 +60,9 @@ impl Default for AlignmentOptions { /// Prior to re-line-breaking or re-aligning, [`unjustify`] has to be called. pub(crate) fn align( layout: &mut LayoutData, - alignment_width: Option, alignment: Alignment, options: AlignmentOptions, ) { - layout.alignment_width = alignment_width.unwrap_or(layout.width); layout.is_aligned_justified = alignment == Alignment::Justify; align_impl::<_, false>(layout, alignment, options); @@ -109,8 +107,8 @@ fn align_impl( } // Compute free space. - let free_space = - layout.alignment_width - line.metrics.advance + line.metrics.trailing_whitespace; + let line_width = line.metrics.inline_max_coord - line.metrics.inline_min_coord; + let free_space = line_width - line.metrics.advance + line.metrics.trailing_whitespace; if !options.align_when_overflowing && free_space <= 0.0 { if is_rtl { diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 9688a49b..9e96fca4 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/layout/cluster.rs @@ -79,7 +79,7 @@ impl<'a, B: Brush> Cluster<'a, B> { let mut path = ClusterPath::default(); if let Some((line_index, line)) = layout.line_for_offset(y) { path.line_index = line_index as u32; - let mut offset = line.metrics().offset; + let mut offset = line.metrics().offset + line.metrics().inline_min_coord; let last_run_index = line.len().saturating_sub(1); for item in line.items_nonpositioned() { match item { @@ -553,8 +553,7 @@ mod tests { fn cluster_from_position_with_alignment(alignment: Alignment) { let mut layout = create_unaligned_layout(); - let width = layout.full_width(); - layout.align(Some(width + 100.), alignment, AlignmentOptions::default()); + layout.align(alignment, AlignmentOptions::default()); assert_eq!( layout.len(), 1, diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs index 638291aa..d5539c46 100644 --- a/parley/src/layout/data.rs +++ b/parley/src/layout/data.rs @@ -5,7 +5,7 @@ use crate::inline_box::InlineBox; use crate::layout::{ContentWidths, Glyph, LineMetrics, RunMetrics, Style}; use crate::style::Brush; use crate::util::nearly_zero; -use crate::{FontData, LineHeight, OverflowWrap}; +use crate::{FontData, InlineBoxKind, LineHeight, OverflowWrap}; use core::ops::Range; use swash::text::cluster::{Boundary, Whitespace}; @@ -571,8 +571,10 @@ impl LayoutData { } LayoutItemKind::InlineBox => { let ibox = &self.inline_boxes[item.index]; - min_width = min_width.max(ibox.width); - running_max_width += ibox.width; + if ibox.kind == InlineBoxKind::InFlow { + min_width = min_width.max(ibox.width); + running_max_width += ibox.width; + } prev_cluster = None; } } diff --git a/parley/src/layout/layout.rs b/parley/src/layout/layout.rs index bee1cff8..a7fa5229 100644 --- a/parley/src/layout/layout.rs +++ b/parley/src/layout/layout.rs @@ -35,6 +35,11 @@ impl Layout { &self.data.styles } + /// Returns available width of the layout. + pub fn available_width(&self) -> f32 { + self.data.alignment_width + } + /// Returns the width of the layout. pub fn width(&self) -> f32 { self.data.width @@ -126,14 +131,9 @@ impl Layout { /// You must perform line breaking prior to aligning, through [`Layout::break_lines`] or /// [`Layout::break_all_lines`]. If `container_width` is not specified, the layout's /// [`Layout::width`] is used. - pub fn align( - &mut self, - container_width: Option, - alignment: Alignment, - options: AlignmentOptions, - ) { + pub fn align(&mut self, alignment: Alignment, options: AlignmentOptions) { unjustify(&mut self.data); - align(&mut self.data, container_width, alignment, options); + align(&mut self.data, alignment, options); } /// Returns the index and `Line` object for the line containing the @@ -166,9 +166,9 @@ impl Layout { return Some((0, self.get(0)?)); } let maybe_line_index = self.data.lines.binary_search_by(|line| { - if offset < line.metrics.min_coord { + if offset < line.metrics.block_min_coord { Ordering::Greater - } else if offset >= line.metrics.max_coord { + } else if offset >= line.metrics.block_max_coord { Ordering::Less } else { Ordering::Equal diff --git a/parley/src/layout/line.rs b/parley/src/layout/line.rs index d9761f5a..f3174b15 100644 --- a/parley/src/layout/line.rs +++ b/parley/src/layout/line.rs @@ -1,7 +1,6 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::InlineBox; use crate::layout::Style; use crate::layout::data::BreakReason; use crate::layout::data::{LayoutItemKind, LineData}; @@ -9,6 +8,7 @@ use crate::layout::glyph::Glyph; use crate::layout::layout::Layout; use crate::layout::run::Run; use crate::style::Brush; +use crate::{InlineBox, InlineBoxKind}; use core::ops::Range; /// Line in a text layout. @@ -123,16 +123,24 @@ pub struct LineMetrics { pub advance: f32, /// Advance of trailing whitespace. pub trailing_whitespace: f32, + /// Minimum coordinate in the line direction. + /// + /// For horizontal text, this would be the left of the line. + pub inline_min_coord: f32, + /// Maximum coordinate in the line direction. + /// + /// For horizontal text, this would be the right of the line. + pub inline_max_coord: f32, /// Minimum coordinate in the direction orthogonal to line /// direction. /// /// For horizontal text, this would be the top of the line. - pub min_coord: f32, + pub block_min_coord: f32, /// Maximum coordinate in the direction orthogonal to line /// direction. /// /// For horizontal text, this would be the bottom of the line. - pub max_coord: f32, + pub block_max_coord: f32, } impl LineMetrics { @@ -173,6 +181,7 @@ pub struct PositionedInlineBox { pub width: f32, pub height: f32, pub id: u64, + pub kind: InlineBoxKind, } /// Sequence of fully positioned glyphs with the same style. @@ -251,17 +260,22 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { let item = self.line.item(self.item_index)?; match item { LineItem::InlineBox(inline_box) => { - let x = self.offset + self.line.data.metrics.offset; + let x = self.offset + + self.line.data.metrics.inline_min_coord + + self.line.data.metrics.offset; self.item_index += 1; self.glyph_start = 0; - self.offset += inline_box.width; + if inline_box.kind == InlineBoxKind::InFlow { + self.offset += inline_box.width; + } return Some(PositionedLayoutItem::InlineBox(PositionedInlineBox { x, y: self.line.data.metrics.baseline - inline_box.height, width: inline_box.width, height: inline_box.height, id: inline_box.id, + kind: inline_box.kind, })); } LineItem::Run(run) => { @@ -288,7 +302,9 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { style, glyph_start, glyph_count, - offset: offset + self.line.data.metrics.offset, + offset: offset + + self.line.data.metrics.inline_min_coord + + self.line.data.metrics.offset, baseline: self.line.data.metrics.baseline, advance, })); diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index efb88af3..2e5f43d1 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -10,12 +10,12 @@ use swash::text::cluster::Whitespace; #[allow(unused_imports)] use core_maths::CoreFloat; -use crate::OverflowWrap; use crate::layout::{ BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData, LineMetrics, Run, }; use crate::style::Brush; +use crate::{InlineBoxKind, OverflowWrap}; use swash::text::cluster::Boundary; use core::ops::Range; @@ -42,6 +42,7 @@ struct LineState { /// Of the line currently being built, the maximum line height seen so far. /// This represents a lower-bound on the eventual line height of the line. running_line_height: f32, + max_height_exceeded: bool, } #[derive(Clone, Default)] @@ -52,8 +53,38 @@ struct PrevBoundaryState { state: LineState, } -#[derive(Clone, Default)] -struct BreakerState { +/// Reason that the line breaker has yielded control flow +pub enum YieldData { + /// Control flow was yielded because a line break was encountered + LineBreak(LineBreakData), + /// Control flow was yielded because content on the line caused the line to exceed the max height + MaxHeightExceeded(MaxHeightBreakData), + /// Control flow was yielded because an inline box with kind `InlineBoxKind::CustomOutOfFlow` + /// was encountered. + InlineBoxBreak(BoxBreakData), +} + +pub struct LineBreakData { + pub reason: BreakReason, + pub advance: f32, + pub line_height: f32, +} + +pub struct MaxHeightBreakData { + pub advance: f32, + pub line_height: f32, +} + +pub struct BoxBreakData { + /// The user-supplied ID for the inline box + pub inline_box_id: u64, + // The index of the inline box within the inline_boxes `Vec` + pub inline_box_index: usize, + pub advance: f32, +} + +#[derive(Clone)] +pub struct BreakerState { /// The number of items that have been processed (used to revert state) items: usize, /// The number of lines that have been processed (used to revert state) @@ -66,18 +97,47 @@ struct BreakerState { /// Iteration state: the current cluster (within the layout) cluster_idx: usize, - /// The y coordinate of the bottom of the last committed line (or else 0) + /// The x coordinate of the left/start of the current line + line_x: f32, + /// The y coordinate of the top/start of the current line /// Use of f64 here is important. f32 causes test failures due to accumulated error - committed_y: f64, + line_y: f64, + + /// The max advance of the entire layout. + layout_max_advance: f32, + /// The max advance (max width) of the current line. This must be <= the `layout_max_advance`. + line_max_advance: f32, + /// The max height available to the current line. + line_max_height: f32, line: LineState, prev_boundary: Option, emergency_boundary: Option, } +impl Default for BreakerState { + fn default() -> Self { + Self { + items: 0, + lines: 0, + item_idx: 0, + run_idx: 0, + cluster_idx: 0, + line_x: 0.0, + line_y: 0.0, + layout_max_advance: 0.0, + line_max_advance: 0.0, + line_max_height: f32::MAX, + line: LineState::default(), + prev_boundary: None, + emergency_boundary: None, + } + } +} + impl BreakerState { /// Add the cluster(s) currently being evaluated to the current line - fn append_cluster_to_line(&mut self, next_x: f32, clusters_height: f32) { + pub fn append_cluster_to_line(&mut self, next_x: f32, clusters_height: f32) { self.line.items.end = self.item_idx + 1; self.line.clusters.end = self.cluster_idx + 1; self.line.x = next_x; @@ -87,7 +147,7 @@ impl BreakerState { } /// Add inline box to line - fn append_inline_box_to_line(&mut self, next_x: f32, box_height: f32) { + pub fn append_inline_box_to_line(&mut self, next_x: f32, box_height: f32) { // self.item_idx += 1; self.line.items.end += 1; self.line.x = next_x; @@ -98,7 +158,7 @@ impl BreakerState { /// Store the current iteration state so that we can revert to it if we later want to take /// the line breaking opportunity at this point. - fn mark_line_break_opportunity(&mut self) { + pub fn mark_line_break_opportunity(&mut self) { self.prev_boundary = Some(PrevBoundaryState { item_idx: self.item_idx, run_idx: self.run_idx, @@ -109,7 +169,7 @@ impl BreakerState { /// Store the current iteration state so that we can revert to it if we later want to take /// an *emergency* line breaking opportunity at this point. - fn mark_emergency_break_opportunity(&mut self) { + pub fn mark_emergency_break_opportunity(&mut self) { self.emergency_boundary = Some(PrevBoundaryState { item_idx: self.item_idx, run_idx: self.run_idx, @@ -121,6 +181,29 @@ impl BreakerState { #[inline(always)] fn add_line_height(&mut self, height: f32) { self.line.running_line_height = self.line.running_line_height.max(height); + self.line.max_height_exceeded = self.line.running_line_height > self.line_max_height; + } + + pub fn set_layout_max_advance(&mut self, advance: f32) { + self.layout_max_advance = advance; + } + + pub fn set_line_max_advance(&mut self, advance: f32) { + self.line_max_advance = advance; + } + + pub fn line_x(&self) -> f32 { + self.line_x + } + pub fn set_line_x(&mut self, x: f32) { + self.line_x = x; + } + + pub fn line_y(&self) -> f64 { + self.line_y + } + pub fn set_line_y(&mut self, y: f64) { + self.line_y = y; } } @@ -151,7 +234,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { } /// Reset state when a line has been committed - fn start_new_line(&mut self) -> Option<(f32, f32)> { + fn start_new_line(&mut self, reason: BreakReason) -> Option { let line_height = self.state.line.running_line_height; self.state.items = self.lines.line_items.len(); @@ -162,17 +245,54 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.state.emergency_boundary = None; self.finish_line(self.lines.lines.len() - 1, line_height); - self.last_line_data() + Some(YieldData::LineBreak(self.last_line_data(reason))) } - fn last_line_data(&self) -> Option<(f32, f32)> { + fn last_line_data(&self, reason: BreakReason) -> LineBreakData { let line = self.lines.lines.last().unwrap(); - Some((line.metrics.advance, line.size())) + LineBreakData { + reason, + advance: line.metrics.advance, + line_height: line.size(), + } + } + + fn max_height_break_data(&self, line_height: f32) -> Option { + Some(YieldData::MaxHeightExceeded(MaxHeightBreakData { + advance: self.state.line.x, + line_height, + })) + } + + pub fn state(&self) -> &BreakerState { + &self.state + } + + pub fn state_mut(&mut self) -> &mut BreakerState { + &mut self.state + } + + /// Reverts the to an externally saved state. + pub fn revert_to(&mut self, state: BreakerState) { + self.state = state; + self.lines.lines.truncate(self.state.lines); + self.lines.line_items.truncate(self.state.items); + self.done = false; + } + + /// Reverts the last computed line, returning to the previous state. + pub fn revert(&mut self) -> bool { + if let Some(state) = self.prev_state.take() { + self.revert_to(state); + true + } else { + false + } } /// Returns the y-coordinate of the top of the current line pub fn committed_y(&self) -> f64 { - self.state.committed_y + self.state.line_y } /// Returns true if all the text has been placed into lines. @@ -182,7 +302,13 @@ impl<'a, B: Brush> BreakLines<'a, B> { /// Computes the next line in the paragraph. Returns the advance and size /// (width and height for horizontal layouts) of the line. - pub fn break_next(&mut self, max_advance: f32) -> Option<(f32, f32)> { + pub fn break_next(&mut self) -> Option { + self.break_next_line_or_box() + } + + /// Computes the next line in the paragraph. Returns the advance and size + /// (width and height for horizontal layouts) of the line. + fn break_next_line_or_box(&mut self) -> Option { // Maintain iterator state if self.done { return None; @@ -195,7 +321,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { if self.layout.data.text_len == 0 && self.layout.data.inline_boxes.is_empty() { f32::MAX } else { - max_advance + self.state.line_max_advance }; // This macro simply calls the `commit_line` with the provided arguments and some parts of self. @@ -233,11 +359,31 @@ impl<'a, B: Brush> BreakLines<'a, B> { LayoutItemKind::InlineBox => { let inline_box = &self.layout.data.inline_boxes[item.index]; + let (width_contribution, height_contribution) = match inline_box.kind { + InlineBoxKind::InFlow => (inline_box.width, inline_box.height), + InlineBoxKind::OutOfFlow => (0.0, 0.0), + // If the box is a `CustomOutOfFlow` box then we yield control flow back to the caller. + // It is then the caller's responsibility to handle placement of the box. + InlineBoxKind::CustomOutOfFlow => { + self.state.item_idx += 1; + return Some(YieldData::InlineBoxBreak(BoxBreakData { + inline_box_id: inline_box.id, + inline_box_index: item.index, + advance: self.state.line.x, + })); + } + }; + // Compute the x position of the content being currently processed - let next_x = self.state.line.x + inline_box.width; + let next_x = self.state.line.x + width_contribution; // println!("BOX next_x: {}", next_x); + let box_will_be_appended = next_x <= max_advance || self.state.line.x == 0.0; + if height_contribution > self.state.line_max_height && box_will_be_appended { + return self.max_height_break_data(height_contribution); + } + // If the box fits on the current line (or we are at the start of the current line) // then simply move on to the next item if next_x <= max_advance { @@ -246,7 +392,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.state.item_idx += 1; self.state - .append_inline_box_to_line(next_x, inline_box.height); + .append_inline_box_to_line(next_x, height_contribution); // We can always line break after an inline box self.state.mark_line_break_opportunity(); @@ -255,15 +401,15 @@ impl<'a, B: Brush> BreakLines<'a, B> { if self.state.line.x == 0.0 { // println!("BOX EMERGENCY BREAK"); self.state - .append_inline_box_to_line(next_x, inline_box.height); + .append_inline_box_to_line(next_x, height_contribution); if try_commit_line!(BreakReason::Emergency) { self.state.item_idx += 1; - return self.start_new_line(); + return self.start_new_line(BreakReason::Emergency); } } else { // println!("BOX BREAK"); if try_commit_line!(BreakReason::Regular) { - return self.start_new_line(); + return self.start_new_line(BreakReason::Regular); } } } @@ -288,6 +434,8 @@ impl<'a, B: Brush> BreakLines<'a, B> { let is_newline = whitespace == Whitespace::Newline; let is_space = whitespace.is_space_or_nbsp(); let boundary = cluster.info().boundary(); + let line_height = run.metrics().line_height; + let max_height_exceeded = self.state.line.max_height_exceeded; let style = &self.layout.data.styles[cluster.data.style_index as usize]; if boundary == Boundary::Line { @@ -300,14 +448,15 @@ impl<'a, B: Brush> BreakLines<'a, B> { // break_opportunity = true; } } else if is_newline { - self.state.append_cluster_to_line( - self.state.line.x, - run.metrics().line_height, - ); + if max_height_exceeded { + return self.max_height_break_data(line_height); + } + self.state + .append_cluster_to_line(self.state.line.x, line_height); if try_commit_line!(BreakReason::Explicit) { // TODO: can this be hoisted out of the conditional? self.state.cluster_idx += 1; - return self.start_new_line(); + return self.start_new_line(BreakReason::Explicit); } } else if // This text can contribute "emergency" line breaks. @@ -343,7 +492,9 @@ impl<'a, B: Brush> BreakLines<'a, B> { // If that x position does NOT exceed max_advance then we simply add the cluster(s) to the current line if next_x <= max_advance { - let line_height = run.metrics().line_height; + if max_height_exceeded { + return self.max_height_break_data(line_height); + } self.state.append_cluster_to_line(next_x, line_height); self.state.cluster_idx += 1; if is_space { @@ -354,12 +505,14 @@ impl<'a, B: Brush> BreakLines<'a, B> { else { // Handle case where cluster is space character. Hang overflowing whitespace. if is_space { - let line_height = run.metrics().line_height; + if max_height_exceeded { + return self.max_height_break_data(line_height); + } self.state.append_cluster_to_line(next_x, line_height); if try_commit_line!(BreakReason::Regular) { // TODO: can this be hoisted out of the conditional? self.state.cluster_idx += 1; - return self.start_new_line(); + return self.start_new_line(BreakReason::Regular); } } // Handle the (common) case where we have previously encountered a line-breaking opportunity in the current line @@ -378,7 +531,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.state.run_idx = prev.run_idx; self.state.cluster_idx = prev.cluster_idx; - return self.start_new_line(); + return self.start_new_line(BreakReason::Regular); } } // Otherwise perform an emergency line break @@ -392,10 +545,12 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.state.run_idx = prev_emergency.run_idx; self.state.cluster_idx = prev_emergency.cluster_idx; - return self.start_new_line(); + return self.start_new_line(BreakReason::Emergency); } } else { - let line_height = run.metrics().line_height; + if max_height_exceeded { + return self.max_height_break_data(line_height); + } self.state.append_cluster_to_line(next_x, line_height); self.state.cluster_idx += 1; } @@ -412,25 +567,12 @@ impl<'a, B: Brush> BreakLines<'a, B> { } if try_commit_line!(BreakReason::None) { self.done = true; - return self.start_new_line(); + return self.start_new_line(BreakReason::None); } None } - /// Reverts the last computed line, returning to the previous state. - pub fn revert(&mut self) -> bool { - if let Some(state) = self.prev_state.take() { - self.state = state; - self.lines.lines.truncate(self.state.lines); - self.lines.line_items.truncate(self.state.items); - self.done = false; - true - } else { - false - } - } - /// Breaks all remaining lines with the specified maximum advance. This /// consumes the line breaker. pub fn break_remaining(mut self, max_advance: f32) { @@ -446,8 +588,18 @@ impl<'a, B: Brush> BreakLines<'a, B> { // } // println!("\nBREAK ALL"); - - while self.break_next(max_advance).is_some() {} + self.state.layout_max_advance = max_advance; + self.state.line_max_advance = max_advance; + while let Some(data) = self.break_next() { + match data { + YieldData::LineBreak(line_break_data) => { + self.state.line_y += line_break_data.line_height as f64; + continue; + } + YieldData::MaxHeightExceeded(_) => continue, // unreachable because max_height is set to f32::MAX + YieldData::InlineBoxBreak(_) => continue, + } + } self.finish(); } @@ -492,13 +644,14 @@ impl<'a, B: Brush> BreakLines<'a, B> { let item = &self.layout.data.inline_boxes[line_item.index]; // Advance is already computed in "commit line" for items + if item.kind == InlineBoxKind::InFlow { + // Default vertical alignment is to align the bottom of boxes with the text baseline. + // This is equivalent to the entire height of the box being "ascent" + line.metrics.ascent = line.metrics.ascent.max(item.height); - // Default vertical alignment is to align the bottom of boxes with the text baseline. - // This is equivalent to the entire height of the box being "ascent" - line.metrics.ascent = line.metrics.ascent.max(item.height); - - // Mark us as having seen non-whitespace content on this line - have_metrics = true; + // Mark us as having seen non-whitespace content on this line + have_metrics = true; + } } LayoutItemKind::TextRun => { line_item.compute_whitespace_properties(&self.layout.data); @@ -638,7 +791,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { (line.metrics.leading * 0.5, line.metrics.leading * 0.5) }; - let y = self.state.committed_y; + let y = self.state.line_y; line.metrics.baseline = ascent + leading_above + if quantize { y.round() as f32 } else { y as f32 }; @@ -646,10 +799,17 @@ impl<'a, B: Brush> BreakLines<'a, B> { // Negative leadings are correct for baseline calculation, but not for min/max coords. // We clamp leading to zero for the purposes of min/max coords, // which in turn clamps the selection box minimum height to ascent + descent. - line.metrics.min_coord = line.metrics.baseline - ascent - leading_above.max(0.); - line.metrics.max_coord = line.metrics.baseline + descent + leading_below.max(0.); + line.metrics.block_min_coord = line.metrics.baseline - ascent - leading_above.max(0.); + line.metrics.block_max_coord = line.metrics.baseline + descent + leading_below.max(0.); - self.state.committed_y += line.metrics.line_height as f64; + let max_advance = if self.state.line_max_advance < f32::MAX { + self.state.line_max_advance + } else { + line.metrics.advance - line.metrics.trailing_whitespace + }; + + line.metrics.inline_min_coord = self.state.line_x; + line.metrics.inline_max_coord = self.state.line_x + max_advance; } } @@ -666,13 +826,31 @@ impl Drop for BreakLines<'_, B> { height += line.metrics.line_height as f64; } + // If laying out with infinite width constraint, then set all lines' "max_width" + // to the measured width of the longest line. + if self.state.layout_max_advance >= f32::MAX { + self.layout.data.alignment_width = full_width; + for line in &mut self.lines.lines { + line.metrics.inline_max_coord = line.metrics.inline_min_coord + width; + } + } else { + self.layout.data.alignment_width = self.state.layout_max_advance; + } + + // Don't include the last line's line_height in the layout's height if the last line is empty + if let Some(last_line) = self.lines.lines.last() { + if last_line.item_range.is_empty() { + height -= last_line.metrics.line_height as f64; + } + } + // Save the computed widths/height to the layout self.layout.data.width = width; self.layout.data.full_width = full_width; self.layout.data.height = height as f32; // for (i, line) in self.lines.lines.iter().enumerate() { - // println!("LINE {i}"); + // println!("LINE {i} (h:{})", line.metrics.line_height); // for item_idx in line.item_range.clone() { // let item = &self.lines.line_items[item_idx]; // println!(" ITEM {:?} ({})", item.kind, item.advance); diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 1d66a11b..aa5bf7f3 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -29,7 +29,9 @@ pub use data::BreakReason; pub use glyph::Glyph; pub use layout::Layout; pub use line::{GlyphRun, Line, LineMetrics, PositionedInlineBox, PositionedLayoutItem}; -pub use line_break::BreakLines; +pub use line_break::{ + BoxBreakData, BreakLines, BreakerState, LineBreakData, MaxHeightBreakData, YieldData, +}; pub use run::{Run, RunMetrics}; pub(crate) use data::{LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData}; diff --git a/parley/src/lib.rs b/parley/src/lib.rs index fb9ba03f..07e5ad32 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -23,8 +23,8 @@ //! //! ```rust //! use parley::{ -//! Alignment, AlignmentOptions, FontContext, FontWeight, InlineBox, Layout, LayoutContext, -//! LineHeight, PositionedLayoutItem, StyleProperty, +//! Alignment, AlignmentOptions, FontContext, FontWeight, InlineBox, InlineBoxKind, Layout, +//! LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, //! }; //! //! // Create a FontContext (font database) and LayoutContext (scratch space). @@ -44,7 +44,7 @@ //! builder.push(StyleProperty::FontWeight(FontWeight::new(600.0)), 0..4); //! //! // Add a box to be laid out inline with the text -//! builder.push_inline_box(InlineBox { id: 0, index: 5, width: 50.0, height: 50.0 }); +//! builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, index: 5, width: 50.0, height: 50.0 }); //! //! // Build the builder into a Layout //! let mut layout: Layout<()> = builder.build(&TEXT); @@ -52,7 +52,7 @@ //! // Run line-breaking and alignment on the Layout //! const MAX_WIDTH : Option = Some(100.0); //! layout.break_all_lines(MAX_WIDTH); -//! layout.align(MAX_WIDTH, Alignment::Start, AlignmentOptions::default()); +//! layout.align(Alignment::Start, AlignmentOptions::default()); //! //! // Inspect computed layout (see examples for more details) //! let width = layout.width(); @@ -127,7 +127,7 @@ pub use util::BoundingBox; pub use builder::{RangedBuilder, TreeBuilder}; pub use context::LayoutContext; pub use font::FontContext; -pub use inline_box::InlineBox; +pub use inline_box::{InlineBox, InlineBoxKind}; #[doc(inline)] pub use layout::Layout; diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs index f00ebb89..47ce7893 100644 --- a/parley/src/tests/test_basic.rs +++ b/parley/src/tests/test_basic.rs @@ -10,7 +10,7 @@ use peniko::{ use crate::{ Alignment, AlignmentOptions, ContentWidths, FontFamily, FontSettings, FontStack, InlineBox, - Layout, LineHeight, StyleProperty, TextStyle, WhiteSpaceCollapse, test_name, + InlineBoxKind, Layout, LineHeight, StyleProperty, TextStyle, WhiteSpaceCollapse, test_name, }; use super::utils::{ColorBrush, FONT_STACK, TestEnv, asserts::assert_eq_layout_data_alignments}; @@ -23,7 +23,7 @@ fn plain_multiline_text() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(None); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -42,13 +42,14 @@ fn placing_inboxes() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: position, width: 10.0, height: 10.0, }); let mut layout = builder.build(text); layout.break_all_lines(None); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name(test_case_name).check_layout_snapshot(&layout); } } @@ -62,6 +63,7 @@ fn only_inboxes_wrap() { for id in 0..10 { builder.push_inline_box(InlineBox { id, + kind: InlineBoxKind::InFlow, index: 0, width: 10.0, height: 10.0, @@ -69,7 +71,7 @@ fn only_inboxes_wrap() { } let mut layout = builder.build(text); layout.break_all_lines(Some(40.0)); - layout.align(None, Alignment::Center, AlignmentOptions::default()); + layout.align(Alignment::Center, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -83,25 +85,28 @@ fn full_width_inbox() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 1, width: 10., height: 10.0, }); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, index: 1, width, height: 10.0, }); builder.push_inline_box(InlineBox { id: 2, + kind: InlineBoxKind::InFlow, index: 2, width, height: 10.0, }); let mut layout = builder.build(text); layout.break_all_lines(Some(100.)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name(test_case_name).check_layout_snapshot(&layout); } } @@ -113,6 +118,7 @@ fn inbox_separated_by_whitespace() { let mut builder = env.tree_builder(); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 0, width: 10., height: 10.0, @@ -120,6 +126,7 @@ fn inbox_separated_by_whitespace() { builder.push_text(" "); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, index: 1, width: 10.0, height: 10.0, @@ -127,6 +134,7 @@ fn inbox_separated_by_whitespace() { builder.push_text(" "); builder.push_inline_box(InlineBox { id: 2, + kind: InlineBoxKind::InFlow, index: 2, width: 10.0, height: 10.0, @@ -134,13 +142,14 @@ fn inbox_separated_by_whitespace() { builder.push_text(" "); builder.push_inline_box(InlineBox { id: 3, + kind: InlineBoxKind::InFlow, index: 3, width: 10.0, height: 10.0, }); let (mut layout, _text) = builder.build(); layout.break_all_lines(Some(100.)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -152,7 +161,7 @@ fn trailing_whitespace() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(45.)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); assert!( layout.width() < layout.full_width(), @@ -181,7 +190,7 @@ fn leading_whitespace() { builder.push_text(" Line 2"); let (mut layout, _) = builder.build(); layout.break_all_lines(None); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name(test_case_name).check_layout_snapshot(&layout); } } @@ -252,7 +261,7 @@ fn base_level_alignment_ltr() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(150.0)); - layout.align(Some(150.0), alignment, AlignmentOptions::default()); + layout.align(alignment, AlignmentOptions::default()); env.with_name(test_case_name).check_layout_snapshot(&layout); } } @@ -271,7 +280,7 @@ fn base_level_alignment_rtl() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(150.0)); - layout.align(None, alignment, AlignmentOptions::default()); + layout.align(alignment, AlignmentOptions::default()); env.with_name(test_case_name).check_layout_snapshot(&layout); } } @@ -286,7 +295,7 @@ fn overflow_alignment_rtl() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(1000.0)); - layout.align(Some(10.), Alignment::Center, AlignmentOptions::default()); + layout.align(Alignment::Center, AlignmentOptions::default()); env.rendering_config().size = Some(Size::new(10., layout.height().into())); env.check_layout_snapshot(&layout); } @@ -313,7 +322,7 @@ And, finally, yet another sentence."#; let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(150.0)); - layout.align(None, Alignment::Justify, AlignmentOptions::default()); + layout.align(Alignment::Justify, AlignmentOptions::default()); env.with_name(test_case_name).check_layout_snapshot(&layout); } } @@ -333,11 +342,11 @@ fn content_widths() { } = layout.calculate_content_widths(); layout.break_all_lines(Some(min_content_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name("min").check_layout_snapshot(&layout); layout.break_all_lines(Some(max_content_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name("max").check_layout_snapshot(&layout); } @@ -356,11 +365,11 @@ fn content_widths_rtl() { } = layout.calculate_content_widths(); layout.break_all_lines(Some(min_content_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name("min").check_layout_snapshot(&layout); layout.break_all_lines(Some(max_content_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); assert!( layout.width() <= max_content_width, "Layout should never be wider than the max content width" @@ -377,6 +386,7 @@ fn inbox_content_width() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 3, width: 100.0, height: 10.0, @@ -387,7 +397,7 @@ fn inbox_content_width() { .. } = layout.calculate_content_widths(); layout.break_all_lines(Some(min_content_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name("full_width").check_layout_snapshot(&layout); } @@ -397,6 +407,7 @@ fn inbox_content_width() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 2, width: 10.0, height: 10.0, @@ -407,7 +418,7 @@ fn inbox_content_width() { .. } = layout.calculate_content_widths(); layout.break_all_lines(Some(max_content_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); assert!( layout.width() <= max_content_width, @@ -427,7 +438,7 @@ fn ligatures_ltr() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(100.0)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); let line = layout.lines().next().unwrap(); let item = line.items().next().unwrap(); @@ -474,7 +485,7 @@ fn ligatures_rtl() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(100.0)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); let line = layout.lines().next().unwrap(); let item = line.items().next().unwrap(); @@ -553,7 +564,7 @@ fn text_range_rtl() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(Some(100.0)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); for line in layout.lines() { for item in line.items() { @@ -590,7 +601,7 @@ fn font_features() { ); let mut layout = builder.build(&text); layout.break_all_lines(Some(100.0)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -613,7 +624,7 @@ fn variable_fonts() { ))); let mut layout = builder.build(text); layout.break_all_lines(Some(100.0)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -632,7 +643,7 @@ fn realign() { if [2, 3, 4].contains(&idx) { layout.break_all_lines(Some(150.0)); } - layout.align(Some(150.), Alignment::Justify, AlignmentOptions::default()); + layout.align(Alignment::Justify, AlignmentOptions::default()); } env.check_layout_snapshot(&layout); } @@ -686,7 +697,7 @@ fn realign_all() { let builder = env.ranged_builder(text); let mut layout = builder.build(text); layout.break_all_lines(max_advance); - layout.align(Some(150.), alignment, opts); + layout.align(alignment, opts); layouts.push(layout); } } @@ -714,7 +725,7 @@ fn realign_all() { if max_advance != top_max_advance { top_layout.break_all_lines(top_max_advance); } - top_layout.align(Some(150.), top_alignment, top_opts); + top_layout.align(top_alignment, top_opts); let top_name = format!("{text_name}_{align_name}_{opts_name}_{ma_name}"); //env.with_name(&top_name).check_layout_snapshot(&top_layout); diff --git a/parley/src/tests/test_lines.rs b/parley/src/tests/test_lines.rs index 6813283b..359acb67 100644 --- a/parley/src/tests/test_lines.rs +++ b/parley/src/tests/test_lines.rs @@ -7,8 +7,8 @@ use peniko::kurbo::Size; use super::utils::{ColorBrush, TestEnv}; use crate::{ - Affinity, BoundingBox, Brush, Cursor, InlineBox, Layout, LineHeight, Selection, StyleProperty, - test_name, + Affinity, BoundingBox, Brush, Cursor, InlineBox, InlineBoxKind, Layout, LineHeight, Selection, + StyleProperty, test_name, }; const TEXT: &str = "Some text here. Let's make\n\ @@ -105,12 +105,14 @@ fn build_layout>>( builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, index: 40, width: 50.0, height: 5.0, }); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, index: 51, width: 50.0, height: 3.0, @@ -159,7 +161,7 @@ fn assert_common_truths( assert_eq!(metrics.ascent, ascent, "expected ascent {ascent}"); assert_eq!(metrics.descent, descent, "expected descent {descent}"); assert_eq!( - metrics.max_coord - metrics.min_coord, + metrics.block_max_coord - metrics.block_min_coord, line_box_height, "expected line box height {line_box_height}" ); @@ -269,14 +271,14 @@ fn lines_integral_line_height_plus_one_leading() { let leading = metrics.leading - (1. - ascent.fract()) - (1. - descent.fract()); assert_eq!(leading, 1., "expected +1 leading"); - let above = line.metrics().baseline - line.metrics().min_coord; + let above = line.metrics().baseline - line.metrics().block_min_coord; assert_eq!( above, ascent.round(), "expected above to be exactly rounded ascent {}", ascent.round() ); - let below = line.metrics().max_coord - line.metrics().baseline; + let below = line.metrics().block_max_coord - line.metrics().baseline; assert_eq!( below, descent.round() + 1., @@ -503,8 +505,8 @@ fn lines_fractional_line_height_positive_leading_internal( metrics.leading ); - let above = metrics.baseline - metrics.min_coord; - let below = metrics.max_coord - metrics.baseline; + let above = metrics.baseline - metrics.block_min_coord; + let below = metrics.block_max_coord - metrics.baseline; let above_leading = above - ascent.round(); let below_leading = below - descent.round(); assert!( @@ -554,11 +556,7 @@ fn lines_line_height_metrics_relative() { let mut layout = builder.build(TEXT); layout.break_all_lines(None); - layout.align( - None, - crate::Alignment::Start, - crate::AlignmentOptions::default(), - ); + layout.align(crate::Alignment::Start, crate::AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -572,11 +570,7 @@ fn lines_line_height_size_relative() { let mut layout = builder.build(TEXT); layout.break_all_lines(None); - layout.align( - None, - crate::Alignment::Start, - crate::AlignmentOptions::default(), - ); + layout.align(crate::Alignment::Start, crate::AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -590,10 +584,6 @@ fn lines_line_height_absolute() { let mut layout = builder.build(TEXT); layout.break_all_lines(None); - layout.align( - None, - crate::Alignment::Start, - crate::AlignmentOptions::default(), - ); + layout.align(crate::Alignment::Start, crate::AlignmentOptions::default()); env.check_layout_snapshot(&layout); } diff --git a/parley/src/tests/test_wrap.rs b/parley/src/tests/test_wrap.rs index 0983014d..fd0f3ad9 100644 --- a/parley/src/tests/test_wrap.rs +++ b/parley/src/tests/test_wrap.rs @@ -47,7 +47,7 @@ fn test_wrap_with_custom_text( let mut layout = builder.build(text); layout.break_all_lines(Some(wrap_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -141,7 +141,7 @@ fn overflow_wrap_anywhere_min_content_width() { let mut layout = builder.build(text); layout.break_all_lines(Some(layout.calculate_content_widths().min)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -156,7 +156,7 @@ fn overflow_wrap_break_word_min_content_width() { let mut layout = builder.build(text); layout.break_all_lines(Some(layout.calculate_content_widths().min)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.check_layout_snapshot(&layout); } @@ -223,7 +223,7 @@ fn word_break_break_all_min_content_width() { let mut layout = builder.build(text); layout.break_all_lines(Some(layout.calculate_content_widths().min)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); // This snapshot will have slightly different line wrapping than the corresponding overflow-wrap test. This is to be // expected and matches browser/CSS behavior. env.check_layout_snapshot(&layout); @@ -258,7 +258,7 @@ fn word_break_keep_all() { let mut layout = builder.build(text); layout.break_all_lines(Some(wrap_width)); - layout.align(None, Alignment::Start, AlignmentOptions::default()); + layout.align(Alignment::Start, AlignmentOptions::default()); env.with_name(name).check_layout_snapshot(&layout); }; diff --git a/parley/src/tests/utils/renderer.rs b/parley/src/tests/utils/renderer.rs index 1cb435e2..137efca5 100644 --- a/parley/src/tests/utils/renderer.rs +++ b/parley/src/tests/utils/renderer.rs @@ -73,14 +73,16 @@ pub(crate) fn render_layout( let width = config .size .map(|size| size.width as f32) - .unwrap_or(layout.width()) + .unwrap_or(layout.available_width()) .ceil() as u32; let height = config .size .map(|size| size.height as f32) .unwrap_or(layout.height()) .ceil() as u32; - let padded_width = width + padding * 2; + + let img_width = width.max(layout.width().ceil() as u32); + let padded_width = img_width + padding * 2; let padded_height = height + padding * 2; let fpadding = padding as f32; @@ -160,12 +162,14 @@ pub(crate) fn render_layout_with_clusters( let width = config .size .map(|size| size.width as f32) - .unwrap_or(layout.width()) + .unwrap_or(layout.available_width()) .ceil() as u32; let base_height = layout.height(); let num_lines = layout.len(); let height = (base_height + (line_extra_spacing * num_lines as f32)).ceil() as u32; - let padded_width = width + padding * 2; + + let img_width = width.max(layout.width().ceil() as u32); + let padded_width = img_width + padding * 2; let padded_height = height + padding * 2; let fpadding = padding as f32; diff --git a/parley/tests/snapshots/base_level_alignment_rtl-center.png b/parley/tests/snapshots/base_level_alignment_rtl-center.png index e5865360..a1ccc4f3 100644 Binary files a/parley/tests/snapshots/base_level_alignment_rtl-center.png and b/parley/tests/snapshots/base_level_alignment_rtl-center.png differ diff --git a/parley/tests/snapshots/base_level_alignment_rtl-end.png b/parley/tests/snapshots/base_level_alignment_rtl-end.png index caed57da..bee318d2 100644 Binary files a/parley/tests/snapshots/base_level_alignment_rtl-end.png and b/parley/tests/snapshots/base_level_alignment_rtl-end.png differ diff --git a/parley/tests/snapshots/base_level_alignment_rtl-justify.png b/parley/tests/snapshots/base_level_alignment_rtl-justify.png index c3233b27..ed211a99 100644 Binary files a/parley/tests/snapshots/base_level_alignment_rtl-justify.png and b/parley/tests/snapshots/base_level_alignment_rtl-justify.png differ diff --git a/parley/tests/snapshots/base_level_alignment_rtl-start.png b/parley/tests/snapshots/base_level_alignment_rtl-start.png index 75a97f92..defc3f4a 100644 Binary files a/parley/tests/snapshots/base_level_alignment_rtl-start.png and b/parley/tests/snapshots/base_level_alignment_rtl-start.png differ diff --git a/parley/tests/snapshots/editor_select_hard_line-0.png b/parley/tests/snapshots/editor_select_hard_line-0.png index 37595f40..994660d7 100644 Binary files a/parley/tests/snapshots/editor_select_hard_line-0.png and b/parley/tests/snapshots/editor_select_hard_line-0.png differ diff --git a/parley/tests/snapshots/editor_select_hard_line-1.png b/parley/tests/snapshots/editor_select_hard_line-1.png index e70c363b..f6cef2e4 100644 Binary files a/parley/tests/snapshots/editor_select_hard_line-1.png and b/parley/tests/snapshots/editor_select_hard_line-1.png differ diff --git a/parley/tests/snapshots/editor_select_hard_line-2.png b/parley/tests/snapshots/editor_select_hard_line-2.png index 64ca0b95..f59e7a9b 100644 Binary files a/parley/tests/snapshots/editor_select_hard_line-2.png and b/parley/tests/snapshots/editor_select_hard_line-2.png differ diff --git a/parley/tests/snapshots/editor_select_hard_line-3.png b/parley/tests/snapshots/editor_select_hard_line-3.png index fda73a7a..3fc18415 100644 Binary files a/parley/tests/snapshots/editor_select_hard_line-3.png and b/parley/tests/snapshots/editor_select_hard_line-3.png differ diff --git a/parley/tests/snapshots/editor_select_hard_line-4.png b/parley/tests/snapshots/editor_select_hard_line-4.png index fda73a7a..3fc18415 100644 Binary files a/parley/tests/snapshots/editor_select_hard_line-4.png and b/parley/tests/snapshots/editor_select_hard_line-4.png differ diff --git a/parley/tests/snapshots/editor_select_hard_line-5.png b/parley/tests/snapshots/editor_select_hard_line-5.png index 2150a507..58b5627c 100644 Binary files a/parley/tests/snapshots/editor_select_hard_line-5.png and b/parley/tests/snapshots/editor_select_hard_line-5.png differ diff --git a/parley/tests/snapshots/editor_select_hard_line-6.png b/parley/tests/snapshots/editor_select_hard_line-6.png index d0390b5d..6fd31fe4 100644 Binary files a/parley/tests/snapshots/editor_select_hard_line-6.png and b/parley/tests/snapshots/editor_select_hard_line-6.png differ diff --git a/parley/tests/snapshots/font_features-0.png b/parley/tests/snapshots/font_features-0.png index d9128b14..31437c27 100644 Binary files a/parley/tests/snapshots/font_features-0.png and b/parley/tests/snapshots/font_features-0.png differ diff --git a/parley/tests/snapshots/full_width_inbox-larger.png b/parley/tests/snapshots/full_width_inbox-larger.png index 00be89b6..010ba063 100644 Binary files a/parley/tests/snapshots/full_width_inbox-larger.png and b/parley/tests/snapshots/full_width_inbox-larger.png differ diff --git a/parley/tests/snapshots/full_width_inbox-smaller.png b/parley/tests/snapshots/full_width_inbox-smaller.png index 8917a4e4..acd26a4c 100644 Binary files a/parley/tests/snapshots/full_width_inbox-smaller.png and b/parley/tests/snapshots/full_width_inbox-smaller.png differ diff --git a/parley/tests/snapshots/inbox_separated_by_whitespace-0.png b/parley/tests/snapshots/inbox_separated_by_whitespace-0.png index a611c233..7f40a711 100644 Binary files a/parley/tests/snapshots/inbox_separated_by_whitespace-0.png and b/parley/tests/snapshots/inbox_separated_by_whitespace-0.png differ diff --git a/parley/tests/snapshots/issue_409_justified_text-last_line_one_word.png b/parley/tests/snapshots/issue_409_justified_text-last_line_one_word.png index c32b1b18..8f2d35d5 100644 Binary files a/parley/tests/snapshots/issue_409_justified_text-last_line_one_word.png and b/parley/tests/snapshots/issue_409_justified_text-last_line_one_word.png differ diff --git a/parley/tests/snapshots/issue_409_justified_text-last_line_three_words.png b/parley/tests/snapshots/issue_409_justified_text-last_line_three_words.png index 606ec77b..36f8f689 100644 Binary files a/parley/tests/snapshots/issue_409_justified_text-last_line_three_words.png and b/parley/tests/snapshots/issue_409_justified_text-last_line_three_words.png differ diff --git a/parley/tests/snapshots/issue_409_justified_text-one_line.png b/parley/tests/snapshots/issue_409_justified_text-one_line.png index a3a91da8..cb1086d6 100644 Binary files a/parley/tests/snapshots/issue_409_justified_text-one_line.png and b/parley/tests/snapshots/issue_409_justified_text-one_line.png differ diff --git a/parley/tests/snapshots/issue_409_justified_text-paragraphs.png b/parley/tests/snapshots/issue_409_justified_text-paragraphs.png index 927c5ab0..b1209237 100644 Binary files a/parley/tests/snapshots/issue_409_justified_text-paragraphs.png and b/parley/tests/snapshots/issue_409_justified_text-paragraphs.png differ diff --git a/parley/tests/snapshots/ligatures_ltr-0.png b/parley/tests/snapshots/ligatures_ltr-0.png index c737904c..18d16f07 100644 Binary files a/parley/tests/snapshots/ligatures_ltr-0.png and b/parley/tests/snapshots/ligatures_ltr-0.png differ diff --git a/parley/tests/snapshots/ligatures_rtl-0.png b/parley/tests/snapshots/ligatures_rtl-0.png index b6075750..457b5fa6 100644 Binary files a/parley/tests/snapshots/ligatures_rtl-0.png and b/parley/tests/snapshots/ligatures_rtl-0.png differ diff --git a/parley/tests/snapshots/overflow_alignment_rtl-0.png b/parley/tests/snapshots/overflow_alignment_rtl-0.png index 1bd33ccf..9fc9a1e0 100644 Binary files a/parley/tests/snapshots/overflow_alignment_rtl-0.png and b/parley/tests/snapshots/overflow_alignment_rtl-0.png differ diff --git a/parley/tests/snapshots/overflow_wrap_during-0.png b/parley/tests/snapshots/overflow_wrap_during-0.png index 9ab96069..124c4b88 100644 Binary files a/parley/tests/snapshots/overflow_wrap_during-0.png and b/parley/tests/snapshots/overflow_wrap_during-0.png differ diff --git a/parley/tests/snapshots/overflow_wrap_everywhere-0.png b/parley/tests/snapshots/overflow_wrap_everywhere-0.png index bbb59288..52c25ecc 100644 Binary files a/parley/tests/snapshots/overflow_wrap_everywhere-0.png and b/parley/tests/snapshots/overflow_wrap_everywhere-0.png differ diff --git a/parley/tests/snapshots/overflow_wrap_first_half-0.png b/parley/tests/snapshots/overflow_wrap_first_half-0.png index c62f4339..7ff9ae0f 100644 Binary files a/parley/tests/snapshots/overflow_wrap_first_half-0.png and b/parley/tests/snapshots/overflow_wrap_first_half-0.png differ diff --git a/parley/tests/snapshots/overflow_wrap_narrow-0.png b/parley/tests/snapshots/overflow_wrap_narrow-0.png index cda123fa..9439936b 100644 Binary files a/parley/tests/snapshots/overflow_wrap_narrow-0.png and b/parley/tests/snapshots/overflow_wrap_narrow-0.png differ diff --git a/parley/tests/snapshots/overflow_wrap_off-0.png b/parley/tests/snapshots/overflow_wrap_off-0.png index f549d922..df87bffc 100644 Binary files a/parley/tests/snapshots/overflow_wrap_off-0.png and b/parley/tests/snapshots/overflow_wrap_off-0.png differ diff --git a/parley/tests/snapshots/overflow_wrap_second_half-0.png b/parley/tests/snapshots/overflow_wrap_second_half-0.png index 16e45d0b..3085b78b 100644 Binary files a/parley/tests/snapshots/overflow_wrap_second_half-0.png and b/parley/tests/snapshots/overflow_wrap_second_half-0.png differ diff --git a/parley/tests/snapshots/trailing_whitespace-0.png b/parley/tests/snapshots/trailing_whitespace-0.png index 4a6eaea3..77bb3248 100644 Binary files a/parley/tests/snapshots/trailing_whitespace-0.png and b/parley/tests/snapshots/trailing_whitespace-0.png differ diff --git a/parley/tests/snapshots/variable_fonts-0.png b/parley/tests/snapshots/variable_fonts-0.png index da2d708e..e12b842b 100644 Binary files a/parley/tests/snapshots/variable_fonts-0.png and b/parley/tests/snapshots/variable_fonts-0.png differ diff --git a/parley/tests/snapshots/variable_fonts-1.png b/parley/tests/snapshots/variable_fonts-1.png index 775a6af1..5cf1ec6a 100644 Binary files a/parley/tests/snapshots/variable_fonts-1.png and b/parley/tests/snapshots/variable_fonts-1.png differ diff --git a/parley/tests/snapshots/variable_fonts-2.png b/parley/tests/snapshots/variable_fonts-2.png index fbd7c7f4..8be189c8 100644 Binary files a/parley/tests/snapshots/variable_fonts-2.png and b/parley/tests/snapshots/variable_fonts-2.png differ diff --git a/parley/tests/snapshots/word_break_break_all_during-0.png b/parley/tests/snapshots/word_break_break_all_during-0.png index 3ed5faea..5563a5fc 100644 Binary files a/parley/tests/snapshots/word_break_break_all_during-0.png and b/parley/tests/snapshots/word_break_break_all_during-0.png differ diff --git a/parley/tests/snapshots/word_break_break_all_everywhere-0.png b/parley/tests/snapshots/word_break_break_all_everywhere-0.png index 14a9cc3e..30caadec 100644 Binary files a/parley/tests/snapshots/word_break_break_all_everywhere-0.png and b/parley/tests/snapshots/word_break_break_all_everywhere-0.png differ diff --git a/parley/tests/snapshots/word_break_break_all_first_half-0.png b/parley/tests/snapshots/word_break_break_all_first_half-0.png index bcf9920c..19a0fb35 100644 Binary files a/parley/tests/snapshots/word_break_break_all_first_half-0.png and b/parley/tests/snapshots/word_break_break_all_first_half-0.png differ diff --git a/parley/tests/snapshots/word_break_break_all_second_half-0.png b/parley/tests/snapshots/word_break_break_all_second_half-0.png index b4b2660b..4b9942d6 100644 Binary files a/parley/tests/snapshots/word_break_break_all_second_half-0.png and b/parley/tests/snapshots/word_break_break_all_second_half-0.png differ diff --git a/parley/tests/snapshots/word_break_keep_all-ID_and_CJ.png b/parley/tests/snapshots/word_break_keep_all-ID_and_CJ.png index fcdeacfd..f1e3d543 100644 Binary files a/parley/tests/snapshots/word_break_keep_all-ID_and_CJ.png and b/parley/tests/snapshots/word_break_keep_all-ID_and_CJ.png differ diff --git a/parley/tests/snapshots/word_break_keep_all-japanese.png b/parley/tests/snapshots/word_break_keep_all-japanese.png index 5718eb56..ff9b31a5 100644 Binary files a/parley/tests/snapshots/word_break_keep_all-japanese.png and b/parley/tests/snapshots/word_break_keep_all-japanese.png differ diff --git a/parley/tests/snapshots/word_break_keep_all-korean.png b/parley/tests/snapshots/word_break_keep_all-korean.png index 5718eb56..ff9b31a5 100644 Binary files a/parley/tests/snapshots/word_break_keep_all-korean.png and b/parley/tests/snapshots/word_break_keep_all-korean.png differ diff --git a/parley/tests/snapshots/word_break_keep_all-korean_hangul_jamos.png b/parley/tests/snapshots/word_break_keep_all-korean_hangul_jamos.png index 5aed4a2f..4f899fd2 100644 Binary files a/parley/tests/snapshots/word_break_keep_all-korean_hangul_jamos.png and b/parley/tests/snapshots/word_break_keep_all-korean_hangul_jamos.png differ diff --git a/parley/tests/snapshots/word_break_keep_all-latin.png b/parley/tests/snapshots/word_break_keep_all-latin.png index cd468521..ef0f2bc6 100644 Binary files a/parley/tests/snapshots/word_break_keep_all-latin.png and b/parley/tests/snapshots/word_break_keep_all-latin.png differ diff --git a/parley/tests/snapshots/word_break_wpt007-0.png b/parley/tests/snapshots/word_break_wpt007-0.png index 32f923f1..1d3bfd6d 100644 Binary files a/parley/tests/snapshots/word_break_wpt007-0.png and b/parley/tests/snapshots/word_break_wpt007-0.png differ diff --git a/parley_bench/src/benches.rs b/parley_bench/src/benches.rs index fea9cf37..5ac8be41 100644 --- a/parley_bench/src/benches.rs +++ b/parley_bench/src/benches.rs @@ -39,11 +39,7 @@ pub fn defaults() -> Vec { let mut layout: Layout = builder.build(text); layout.break_all_lines(Some(MAX_ADVANCE)); - layout.align( - Some(MAX_ADVANCE), - Alignment::Start, - AlignmentOptions::default(), - ); + layout.align(Alignment::Start, AlignmentOptions::default()); black_box(layout); }) @@ -116,11 +112,7 @@ pub fn styled() -> Vec { let mut layout: Layout = builder.build(text); layout.break_all_lines(Some(MAX_ADVANCE)); - layout.align( - Some(MAX_ADVANCE), - Alignment::Start, - AlignmentOptions::default(), - ); + layout.align(Alignment::Start, AlignmentOptions::default()); black_box(layout); })