From b5b83f29c3f37355c6dd292417ca9d66fc0be725 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 22 Sep 2025 12:44:01 +0100 Subject: [PATCH 01/21] Floats WIP Signed-off-by: Nico Burns --- examples/swash_render/src/main.rs | 4 ++ examples/tiny_skia_render/src/main.rs | 1 + parley/src/inline_box.rs | 2 + parley/src/layout/line_break.rs | 53 +++++++++++++++++++++------ parley/src/layout/mod.rs | 2 +- parley/src/lib.rs | 2 +- parley/src/tests/test_basic.rs | 11 ++++++ parley/src/tests/test_lines.rs | 2 + 8 files changed, 63 insertions(+), 14 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 6be0a3c8..f898c2dc 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -94,6 +94,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: 0, width: 50.0, height: 50.0, @@ -103,6 +104,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 1, + break_on_box: false, index: 50, width: 50.0, height: 30.0, @@ -152,12 +154,14 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: 40, width: 50.0, height: 50.0, }); builder.push_inline_box(InlineBox { id: 1, + break_on_box: false, index: 50, width: 50.0, height: 30.0, diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs index 5ad62933..b7b3b562 100644 --- a/examples/tiny_skia_render/src/main.rs +++ b/examples/tiny_skia_render/src/main.rs @@ -88,6 +88,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: 40, width: 50.0, height: 50.0, diff --git a/parley/src/inline_box.rs b/parley/src/inline_box.rs index 16a7a87b..86aabfd3 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 layout should break on this box + pub break_on_box: bool, /// 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, diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index efb88af3..b8186697 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -52,6 +52,25 @@ struct PrevBoundaryState { state: LineState, } +/// 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 an inline box with `break_on_box` set + /// to `true` was encountered + InlineBoxBreak(BoxBreakData), +} + +pub struct LineBreakData { + pub reason: BreakReason, + pub advance: f32, + pub line_height: f32, +} + +pub struct BoxBreakData { + pub inline_box_id: usize, +} + #[derive(Clone, Default)] struct BreakerState { /// The number of items that have been processed (used to revert state) @@ -151,7 +170,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,12 +181,16 @@ 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(), + } } /// Returns the y-coordinate of the top of the current line @@ -182,7 +205,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, max_advance: f32) -> Option { + self.break_next_line_or_box(max_advance) + } + + /// 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, max_advance: f32) -> Option { // Maintain iterator state if self.done { return None; @@ -258,12 +287,12 @@ impl<'a, B: Brush> BreakLines<'a, B> { .append_inline_box_to_line(next_x, inline_box.height); 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); } } } @@ -307,7 +336,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { 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. @@ -359,7 +388,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { 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 +407,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,7 +421,7 @@ 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; @@ -412,7 +441,7 @@ 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 diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 1d66a11b..1e9f6db2 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -29,7 +29,7 @@ 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, LineBreakData, 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..db74d44d 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -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, break_on_box: false, index: 5, width: 50.0, height: 50.0 }); //! //! // Build the builder into a Layout //! let mut layout: Layout<()> = builder.build(&TEXT); diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs index f00ebb89..52baac35 100644 --- a/parley/src/tests/test_basic.rs +++ b/parley/src/tests/test_basic.rs @@ -42,6 +42,7 @@ fn placing_inboxes() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: position, width: 10.0, height: 10.0, @@ -62,6 +63,7 @@ fn only_inboxes_wrap() { for id in 0..10 { builder.push_inline_box(InlineBox { id, + break_on_box: false, index: 0, width: 10.0, height: 10.0, @@ -83,18 +85,21 @@ fn full_width_inbox() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: 1, width: 10., height: 10.0, }); builder.push_inline_box(InlineBox { id: 1, + break_on_box: false, index: 1, width, height: 10.0, }); builder.push_inline_box(InlineBox { id: 2, + break_on_box: false, index: 2, width, height: 10.0, @@ -113,6 +118,7 @@ fn inbox_separated_by_whitespace() { let mut builder = env.tree_builder(); builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, 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, + break_on_box: false, 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, + break_on_box: false, index: 2, width: 10.0, height: 10.0, @@ -134,6 +142,7 @@ fn inbox_separated_by_whitespace() { builder.push_text(" "); builder.push_inline_box(InlineBox { id: 3, + break_on_box: false, index: 3, width: 10.0, height: 10.0, @@ -377,6 +386,7 @@ fn inbox_content_width() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: 3, width: 100.0, height: 10.0, @@ -397,6 +407,7 @@ fn inbox_content_width() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: 2, width: 10.0, height: 10.0, diff --git a/parley/src/tests/test_lines.rs b/parley/src/tests/test_lines.rs index 6813283b..9cf56604 100644 --- a/parley/src/tests/test_lines.rs +++ b/parley/src/tests/test_lines.rs @@ -105,12 +105,14 @@ fn build_layout>>( builder.push_inline_box(InlineBox { id: 0, + break_on_box: false, index: 40, width: 50.0, height: 5.0, }); builder.push_inline_box(InlineBox { id: 1, + break_on_box: false, index: 51, width: 50.0, height: 3.0, From ff932e4eb7a0684403ad4eeac0197ed1365de892 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 26 Sep 2025 14:38:02 +0100 Subject: [PATCH 02/21] Ignore out-of-flow boxes for the purpose of whitespace collapsing --- examples/swash_render/src/main.rs | 6 +++++- examples/tiny_skia_render/src/main.rs | 3 ++- parley/src/builder.rs | 14 +++++++++----- parley/src/inline_box.rs | 8 ++++++++ parley/src/layout/line_break.rs | 15 ++++++++------- parley/src/lib.rs | 2 +- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index f898c2dc..b0610ac3 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, break_on_box: false, index: 0, width: 50.0, @@ -104,6 +105,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 50, width: 50.0, @@ -154,6 +156,7 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 40, width: 50.0, @@ -161,6 +164,7 @@ fn main() { }); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 50, width: 50.0, diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs index b7b3b562..8f60586f 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, break_on_box: false, index: 40, width: 50.0, 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/inline_box.rs b/parley/src/inline_box.rs index 86aabfd3..a58e0fc1 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, /// Whether layout should break on this box pub break_on_box: bool, /// The byte offset into the underlying text string at which the box should be placed. @@ -17,3 +19,9 @@ pub struct InlineBox { /// The height of the box in pixels pub height: f32, } + +#[derive(PartialEq, Debug, Clone)] +pub enum InlineBoxKind { + InFlow, + OutOfFlow, +} diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index b8186697..6e4a7152 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; @@ -521,13 +521,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); diff --git a/parley/src/lib.rs b/parley/src/lib.rs index db74d44d..20c30284 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -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; From a7b7f4b98e2aa6ebe1e09416c1084b39da76292b Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 27 Sep 2025 12:17:30 +0100 Subject: [PATCH 03/21] Rename coords to block Signed-off-by: Nico Burns --- parley/src/editing/cursor.rs | 8 ++++---- parley/src/editing/selection.rs | 6 +++--- parley/src/layout/accessibility.rs | 4 ++-- parley/src/layout/layout.rs | 4 ++-- parley/src/layout/line.rs | 12 ++++++++++-- parley/src/layout/line_break.rs | 4 ++-- parley/src/tests/test_lines.rs | 10 +++++----- 7 files changed, 28 insertions(+), 20 deletions(-) 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/selection.rs b/parley/src/editing/selection.rs index b9ac2f28..ca755b6b 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. 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/layout.rs b/parley/src/layout/layout.rs index bee1cff8..a6c9485c 100644 --- a/parley/src/layout/layout.rs +++ b/parley/src/layout/layout.rs @@ -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..49d81ada 100644 --- a/parley/src/layout/line.rs +++ b/parley/src/layout/line.rs @@ -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 { diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 6e4a7152..26425de6 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -676,8 +676,8 @@ 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; } diff --git a/parley/src/tests/test_lines.rs b/parley/src/tests/test_lines.rs index 9cf56604..a1a08e7b 100644 --- a/parley/src/tests/test_lines.rs +++ b/parley/src/tests/test_lines.rs @@ -161,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}" ); @@ -271,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., @@ -505,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!( From e49af34111c49b521e62ee44b2b1307c469b763a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 29 Sep 2025 16:05:31 +0100 Subject: [PATCH 04/21] Fixup tests (inline box kind) --- parley/src/lib.rs | 6 +++--- parley/src/tests/test_basic.rs | 13 ++++++++++++- parley/src/tests/test_lines.rs | 6 ++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 20c30284..62de4f1c 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, break_on_box: false, index: 5, width: 50.0, height: 50.0 }); +//! builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, break_on_box: false, index: 5, width: 50.0, height: 50.0 }); //! //! // Build the builder into a Layout //! let mut layout: Layout<()> = builder.build(&TEXT); diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs index 52baac35..c8c4e29e 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}; @@ -42,6 +42,7 @@ fn placing_inboxes() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, break_on_box: false, index: position, width: 10.0, @@ -63,6 +64,7 @@ fn only_inboxes_wrap() { for id in 0..10 { builder.push_inline_box(InlineBox { id, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 0, width: 10.0, @@ -85,6 +87,7 @@ fn full_width_inbox() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 1, width: 10., @@ -92,6 +95,7 @@ fn full_width_inbox() { }); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 1, width, @@ -99,6 +103,7 @@ fn full_width_inbox() { }); builder.push_inline_box(InlineBox { id: 2, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 2, width, @@ -118,6 +123,7 @@ fn inbox_separated_by_whitespace() { let mut builder = env.tree_builder(); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 0, width: 10., @@ -126,6 +132,7 @@ fn inbox_separated_by_whitespace() { builder.push_text(" "); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 1, width: 10.0, @@ -134,6 +141,7 @@ fn inbox_separated_by_whitespace() { builder.push_text(" "); builder.push_inline_box(InlineBox { id: 2, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 2, width: 10.0, @@ -142,6 +150,7 @@ fn inbox_separated_by_whitespace() { builder.push_text(" "); builder.push_inline_box(InlineBox { id: 3, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 3, width: 10.0, @@ -386,6 +395,7 @@ fn inbox_content_width() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 3, width: 100.0, @@ -407,6 +417,7 @@ fn inbox_content_width() { let mut builder = env.ranged_builder(text); builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 2, width: 10.0, diff --git a/parley/src/tests/test_lines.rs b/parley/src/tests/test_lines.rs index a1a08e7b..18d3621b 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,6 +105,7 @@ fn build_layout>>( builder.push_inline_box(InlineBox { id: 0, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 40, width: 50.0, @@ -112,6 +113,7 @@ fn build_layout>>( }); builder.push_inline_box(InlineBox { id: 1, + kind: InlineBoxKind::InFlow, break_on_box: false, index: 51, width: 50.0, From f7fe53112c6c051220b8a068009346b7e7a6f7b3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 29 Sep 2025 18:54:28 +0100 Subject: [PATCH 05/21] Allow the caller to set x/y/max_advance for each line Signed-off-by: Nico Burns --- parley/src/layout/line.rs | 4 +- parley/src/layout/line_break.rs | 124 ++++++++++++++++++++++++-------- parley/src/layout/mod.rs | 2 +- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/parley/src/layout/line.rs b/parley/src/layout/line.rs index 49d81ada..5062c169 100644 --- a/parley/src/layout/line.rs +++ b/parley/src/layout/line.rs @@ -296,7 +296,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 26425de6..9889c60b 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -68,11 +68,14 @@ pub struct LineBreakData { } pub struct BoxBreakData { - pub inline_box_id: usize, + /// 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, } #[derive(Clone, Default)] -struct BreakerState { +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) @@ -85,9 +88,16 @@ 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, line: LineState, prev_boundary: Option, @@ -96,7 +106,7 @@ struct BreakerState { 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; @@ -106,7 +116,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; @@ -117,7 +127,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, @@ -128,7 +138,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, @@ -141,6 +151,28 @@ impl BreakerState { fn add_line_height(&mut self, height: f32) { self.line.running_line_height = self.line.running_line_height.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; + } } /// Line breaking support for a paragraph. @@ -193,9 +225,35 @@ impl<'a, B: Brush> BreakLines<'a, B> { } } + 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. @@ -205,13 +263,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 { - self.break_next_line_or_box(max_advance) + 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, max_advance: f32) -> Option { + fn break_next_line_or_box(&mut self) -> Option { // Maintain iterator state if self.done { return None; @@ -224,7 +282,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. @@ -262,6 +320,15 @@ impl<'a, B: Brush> BreakLines<'a, B> { LayoutItemKind::InlineBox => { let inline_box = &self.layout.data.inline_boxes[item.index]; + // If the box is marked as "break_on_box", then the assumption is that the caller will handle placement of the box. + if inline_box.break_on_box { + self.state.item_idx += 1; + return Some(YieldData::InlineBoxBreak(BoxBreakData { + inline_box_id: inline_box.id, + inline_box_index: item.index, + })); + } + // Compute the x position of the content being currently processed let next_x = self.state.line.x + inline_box.width; @@ -447,19 +514,6 @@ impl<'a, B: Brush> BreakLines<'a, B> { 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) { @@ -475,8 +529,17 @@ 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::InlineBoxBreak(_) => continue, + } + } self.finish(); } @@ -668,7 +731,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 }; @@ -679,7 +742,8 @@ impl<'a, B: Brush> BreakLines<'a, B> { 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; + line.metrics.inline_min_coord = self.state.line_x; + line.metrics.inline_max_coord = self.state.line_x + self.state.line_max_advance; } } diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 1e9f6db2..b710b425 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -29,7 +29,7 @@ pub use data::BreakReason; pub use glyph::Glyph; pub use layout::Layout; pub use line::{GlyphRun, Line, LineMetrics, PositionedInlineBox, PositionedLayoutItem}; -pub use line_break::{BoxBreakData, BreakLines, LineBreakData, YieldData}; +pub use line_break::{BoxBreakData, BreakLines, BreakerState, LineBreakData, YieldData}; pub use run::{Run, RunMetrics}; pub(crate) use data::{LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData}; From 0b4bfc8ef34e6517eb3366fa6772fb89004e1c1f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 30 Sep 2025 11:45:57 +0100 Subject: [PATCH 06/21] Add advance to box yield --- parley/src/layout/line.rs | 2 +- parley/src/layout/line_break.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/parley/src/layout/line.rs b/parley/src/layout/line.rs index 5062c169..fb5759d8 100644 --- a/parley/src/layout/line.rs +++ b/parley/src/layout/line.rs @@ -297,7 +297,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { glyph_start, glyph_count, offset: offset - // + self.line.data.metrics.inline_min_coord + + 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 9889c60b..008c717d 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -72,6 +72,7 @@ pub struct BoxBreakData { 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, Default)] @@ -326,6 +327,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { return Some(YieldData::InlineBoxBreak(BoxBreakData { inline_box_id: inline_box.id, inline_box_index: item.index, + advance: self.state.line.x, })); } From 622417f2050cc63aa3679ed588b8f4fccbfbd1fc Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 15 Oct 2025 23:47:33 +0100 Subject: [PATCH 07/21] Align each line to it's own max-width --- examples/swash_render/src/main.rs | 2 +- examples/tiny_skia_render/src/main.rs | 2 +- parley/src/editing/editor.rs | 2 +- parley/src/layout/alignment.rs | 11 +++++++---- parley/src/layout/layout.rs | 9 ++------- parley_bench/src/benches.rs | 12 ++---------- 6 files changed, 14 insertions(+), 24 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index b0610ac3..1ec045fd 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -179,7 +179,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 8f60586f..30d95176 100644 --- a/examples/tiny_skia_render/src/main.rs +++ b/examples/tiny_skia_render/src/main.rs @@ -100,7 +100,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/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/layout/alignment.rs b/parley/src/layout/alignment.rs index a0703bfb..6f5fd4df 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,13 @@ 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; + + // FIXME: width should never be infinite. + if !line_width.is_finite() { + continue; + } if !options.align_when_overflowing && free_space <= 0.0 { if is_rtl { diff --git a/parley/src/layout/layout.rs b/parley/src/layout/layout.rs index a6c9485c..f11cf117 100644 --- a/parley/src/layout/layout.rs +++ b/parley/src/layout/layout.rs @@ -126,14 +126,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 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); }) From b2a2ae2b34058eb089851e296ab98b5759120a69 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 16 Oct 2025 15:39:33 +0100 Subject: [PATCH 08/21] Don't include last line in layout's height if it's empty --- parley/src/layout/line_break.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 008c717d..eb0b3e29 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -762,13 +762,20 @@ impl Drop for BreakLines<'_, B> { height += line.metrics.line_height as f64; } + // 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); From e05ce425a5b5b052f5d9fbb66dc1fc36c992d5f2 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 30 Oct 2025 16:28:26 +0000 Subject: [PATCH 09/21] Fix Cluster::from_point to work with offset lines Signed-off-by: Nico Burns --- parley/src/layout/cluster.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 9688a49b..7e430e75 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 { From e7c65da8411f0f6dd98928bc5eacb22641059b18 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 30 Oct 2025 18:50:50 +0000 Subject: [PATCH 10/21] Remove alignment_width argument from tests --- parley/src/layout/cluster.rs | 3 +- parley/src/lib.rs | 2 +- parley/src/tests/test_basic.rs | 50 +++++++++++++++++----------------- parley/src/tests/test_lines.rs | 18 ++---------- parley/src/tests/test_wrap.rs | 10 +++---- 5 files changed, 35 insertions(+), 48 deletions(-) diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 7e430e75..9e96fca4 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/layout/cluster.rs @@ -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/lib.rs b/parley/src/lib.rs index 62de4f1c..88dec5c1 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -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(); diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs index c8c4e29e..f47a8bec 100644 --- a/parley/src/tests/test_basic.rs +++ b/parley/src/tests/test_basic.rs @@ -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); } @@ -50,7 +50,7 @@ fn placing_inboxes() { }); 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); } } @@ -73,7 +73,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); } @@ -111,7 +111,7 @@ fn full_width_inbox() { }); 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); } } @@ -158,7 +158,7 @@ fn inbox_separated_by_whitespace() { }); 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); } @@ -170,7 +170,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(), @@ -199,7 +199,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); } } @@ -270,7 +270,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); } } @@ -289,7 +289,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); } } @@ -304,7 +304,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); } @@ -331,7 +331,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); } } @@ -351,11 +351,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); } @@ -374,11 +374,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" @@ -407,7 +407,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); } @@ -429,7 +429,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, @@ -449,7 +449,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(); @@ -496,7 +496,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(); @@ -575,7 +575,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() { @@ -612,7 +612,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); } @@ -635,7 +635,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); } @@ -654,7 +654,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); } @@ -708,7 +708,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); } } @@ -736,7 +736,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 18d3621b..20b96512 100644 --- a/parley/src/tests/test_lines.rs +++ b/parley/src/tests/test_lines.rs @@ -558,11 +558,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); } @@ -576,11 +572,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); } @@ -594,10 +586,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); }; From 8e374496b4c68abf4db7a1fd086edc1ef2cdc1f3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 30 Oct 2025 18:54:55 +0000 Subject: [PATCH 11/21] Use inline min coord in more places --- parley/src/editing/selection.rs | 4 ++-- parley/src/layout/line.rs | 4 +++- parley/src/layout/line_break.rs | 8 +++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/parley/src/editing/selection.rs b/parley/src/editing/selection.rs index ca755b6b..cca4aeb6 100644 --- a/parley/src/editing/selection.rs +++ b/parley/src/editing/selection.rs @@ -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/layout/line.rs b/parley/src/layout/line.rs index fb5759d8..bf9a5d52 100644 --- a/parley/src/layout/line.rs +++ b/parley/src/layout/line.rs @@ -259,7 +259,9 @@ 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; diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index eb0b3e29..2508f4f9 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -744,8 +744,14 @@ impl<'a, B: Brush> BreakLines<'a, B> { 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.); + let max_advance = if self.state.line_max_advance.is_finite() { + self.state.line_max_advance + } else { + line.metrics.advance + }; + line.metrics.inline_min_coord = self.state.line_x; - line.metrics.inline_max_coord = self.state.line_x + self.state.line_max_advance; + line.metrics.inline_max_coord = self.state.line_x + max_advance; } } From d6c6cb26fe72b6dc3e0be30d6f86b00161f9dfa7 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 30 Oct 2025 20:05:10 +0000 Subject: [PATCH 12/21] Use layout_max_advance for rendering tests --- parley/src/layout/layout.rs | 5 +++++ parley/src/layout/line_break.rs | 13 ++++++++++++- parley/src/tests/utils/renderer.rs | 12 ++++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/parley/src/layout/layout.rs b/parley/src/layout/layout.rs index f11cf117..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 diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 2508f4f9..6aad4107 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -747,7 +747,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { let max_advance = if self.state.line_max_advance.is_finite() { self.state.line_max_advance } else { - line.metrics.advance + line.metrics.advance - line.metrics.trailing_whitespace }; line.metrics.inline_min_coord = self.state.line_x; @@ -768,6 +768,17 @@ 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::INFINITY { + 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() { 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; From 05186885e81319068a140c6ba5a9066fdddb3d49 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 30 Oct 2025 20:05:46 +0000 Subject: [PATCH 13/21] Accept new snapshots Signed-off-by: Nico Burns --- .../base_level_alignment_rtl-center.png | Bin 2759 -> 2773 bytes .../base_level_alignment_rtl-end.png | Bin 2729 -> 2748 bytes .../base_level_alignment_rtl-justify.png | Bin 2728 -> 2778 bytes .../base_level_alignment_rtl-start.png | Bin 2753 -> 2752 bytes .../snapshots/editor_select_hard_line-0.png | Bin 1482 -> 1519 bytes .../snapshots/editor_select_hard_line-1.png | Bin 1318 -> 1353 bytes .../snapshots/editor_select_hard_line-2.png | Bin 1315 -> 1352 bytes .../snapshots/editor_select_hard_line-3.png | Bin 1762 -> 1762 bytes .../snapshots/editor_select_hard_line-4.png | Bin 1762 -> 1762 bytes .../snapshots/editor_select_hard_line-5.png | Bin 1342 -> 1379 bytes .../snapshots/editor_select_hard_line-6.png | Bin 1483 -> 1521 bytes parley/tests/snapshots/font_features-0.png | Bin 325 -> 336 bytes .../snapshots/full_width_inbox-larger.png | Bin 450 -> 452 bytes .../snapshots/full_width_inbox-smaller.png | Bin 448 -> 452 bytes .../inbox_separated_by_whitespace-0.png | Bin 150 -> 156 bytes ..._409_justified_text-last_line_one_word.png | Bin 1416 -> 1388 bytes ...9_justified_text-last_line_three_words.png | Bin 1524 -> 1516 bytes .../issue_409_justified_text-one_line.png | Bin 678 -> 684 bytes .../issue_409_justified_text-paragraphs.png | Bin 3395 -> 3418 bytes parley/tests/snapshots/ligatures_ltr-0.png | Bin 335 -> 346 bytes parley/tests/snapshots/ligatures_rtl-0.png | Bin 298 -> 309 bytes .../snapshots/overflow_alignment_rtl-0.png | Bin 345 -> 2175 bytes .../snapshots/overflow_wrap_during-0.png | Bin 2738 -> 2740 bytes .../snapshots/overflow_wrap_everywhere-0.png | Bin 2519 -> 2515 bytes .../snapshots/overflow_wrap_first_half-0.png | Bin 2709 -> 2908 bytes .../snapshots/overflow_wrap_narrow-0.png | Bin 1763 -> 1929 bytes .../tests/snapshots/overflow_wrap_off-0.png | Bin 2463 -> 2727 bytes .../snapshots/overflow_wrap_second_half-0.png | Bin 2727 -> 2906 bytes .../tests/snapshots/trailing_whitespace-0.png | Bin 485 -> 489 bytes parley/tests/snapshots/variable_fonts-0.png | Bin 534 -> 543 bytes parley/tests/snapshots/variable_fonts-1.png | Bin 547 -> 555 bytes parley/tests/snapshots/variable_fonts-2.png | Bin 556 -> 561 bytes .../word_break_break_all_during-0.png | Bin 2738 -> 2740 bytes .../word_break_break_all_everywhere-0.png | Bin 2502 -> 2501 bytes .../word_break_break_all_first_half-0.png | Bin 2656 -> 2899 bytes .../word_break_break_all_second_half-0.png | Bin 2727 -> 2906 bytes .../word_break_keep_all-ID_and_CJ.png | Bin 263 -> 269 bytes .../word_break_keep_all-japanese.png | Bin 491 -> 495 bytes .../snapshots/word_break_keep_all-korean.png | Bin 491 -> 495 bytes ...ord_break_keep_all-korean_hangul_jamos.png | Bin 536 -> 545 bytes .../snapshots/word_break_keep_all-latin.png | Bin 663 -> 665 bytes .../tests/snapshots/word_break_wpt007-0.png | Bin 1017 -> 1111 bytes 42 files changed, 0 insertions(+), 0 deletions(-) diff --git a/parley/tests/snapshots/base_level_alignment_rtl-center.png b/parley/tests/snapshots/base_level_alignment_rtl-center.png index e586536007aedf067a5514adb45108f48f7db25c..a1ccc4f326d7075bba9b4030029e705e6ce56e18 100644 GIT binary patch literal 2773 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 b`wsg*a+Vi|D_RHZ00000NkvXXu0mjfnD2ik literal 2759 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 X;z0oj#BolD00000NkvXXu0mjfr_x3e delta 2646 zcmV-c3aRzH6{!^=iBL{Q4GJ0x0000DNk~Le0001^0001p2m=5B0KD0&N0A{%8)0E# zo12?;b#;!8jwv!fQ)GXAmcU(bk7#|Lk%KLN6XO7G000TyNklC*> zXMX?pFR%F9KmYa1JO1al|NiffyE=S`L`nq?I+5DJL->TFQeX%y25w@eV=Xd{@F|pk z+3YB5z`=Q72e*(CJk*kE;Zr2I*|F8oF+Z5Gru~?X``y~{Zj9dQxI`74^CU~@wxb#0 zBWQZq`$&C0?!YqYXxa<_!1ih0)1@Ojj41+8HI`!wj$Ku$=sp_o$kKpsHV{r=_c>Bo z>m=jw268{vz6H=o0KDjQGhR6MSn$DrI@VklXs@aAOx9?18M;v>5h_Xo*UYSX#&u%u zb4)U_Mrv{#?G-hc=~4{M%&|r0Ts-vv09``>GG99a)njmUoBN-2I=F#5=NQJ4a(1b# zn>i*DW{%vN6}Cy^($U+g$+1JY-x1?uMjpoE`Rr0MnK{Y}M}|LlgeI9B`4&fiIRF5m z4vr=a7%$c3Ge=(1%#pCQO#m=7R~>meIhrlcy2FzaV@7uLUVIa2z7stPfv2|}%MNvP zq-@-Y6N(?gF{VF$obV(n?5vDy4>HOguWXC$@Li* z2G=w#I7drx!&no4Y;&H>M6JrilRkd?s`8ZZ;nqrsElMn}qjICu=2g(Ju@KgopC@O^7r&3QKBJ z<-V-W$q@2k3X8r?V`aO2f7_(u#}9$M_-;S$QcRD2+>^UPdY>bI!zFpld9i7p3=MMU z7~8rU7nF9B4Qjv(?sJ3>auje0L%1O2PpnBrjDcGWgI>6E>hk9~G6uIOWTMt#F~ABbIe`)e$oA^!NsbXZ(L5juun2-n zPV~XdzB5m7Y>~l#1Sa9{63ys81$HZzBj=!mLp?goY2L}F-fL+H4SGrGJZgymcJp;>-5y0e_O52h<#~8AIVq%P;e3Z^Hv?2?RE$X1R z-BDxHpb1}pXGevt*IJb$2Z)Y&2bQajNez#3R5$?DImK#eOjLHeEJwz~=!9-Ly3KaS z@Hfq@Xx0NjGkC{XrD~Cu<;Y?`cWjrA2HQz5e}tn<-0+pMGKX*CV8h_#Sd>R~bOnJ~ zIr8*PRM-oDF(sWkJqk;YHaw>G2$nk=Ais+v?uF>nw{v^BvDQoj;z(d*EIB%;K*WL; zwaG;%Q{aNy@MaZCH#j!j+vpoSIFj~uj% z`qxkz4Ex0FG$!DX4P?V(9J9e7a-fckhR?z}SP<*>M4;d?j;>h3;pgZC?qs`Tz&Yv> zEXGz2BR(zWGIkiXlMVqh?I98)V@#RBQB~tOhVhSgJ96}qPfCo}^=UDLK%8}!Az*fV z`G!h=+dG_xJalc##?5@g-i^P>QTeG{1A&kh`BleaUSZQa-UpGgzAMWN7T>Jbw2H$F5m9nwcYOY+AhT7`w$*cWdLp=-47m zj_R|H#jtwTtrLv}NjcthEXIoobxT)LI4FmI=s2xd2;s8*)Uysx2z&9v&0BvH7xu>x0#Vqiqj$9VaA)(OAh0CmPP}o%(e?f=Dj!9Iah=PSF@}uQ)(mERm z92%J`=w$v-z&qj@1{O-PMG4X|Int2L$N!9D79833Z_Mw*a6^97Xy5~=BLE$YIuO8r zaEIg2+?gN8syg5RAA!4fzU%OB)d4kp29qDE9=kj9qkKaP0#D0er(g~Vnonn|zk~S) zlhP}Si@wwk;fz&gCoW~dw*9;gr-!A64l?my*%6LPW*6TSPb?%xQ#KkFD|*&!cVrA& zHQAUej-)}c3rCd3M-m(ep5i#*D!2Z$q^CF*>C7>$$=8*J)Xx+x9k~mWYex??$x|FVjE;nVEjY(u zmh+{fEZ`@OFv!O{F6xWM`*3%A36hMKjy3T!36S7Parnaqg%x4W1Ui#*s1(lnz#5 zewO#>g3UmS_3fm8!jii4YSZ9MLdaELkx~K z9J-Pt_M$&8fK*{#`^U6~@*Fxv{}9Kb0br}5VOF%fj=Pg1UV+8tYbz=LbosHC*>za;+$4^-^Bu2#bh)&Kwi07*qoM6N<$ Ef;Y(ZTmS$7 diff --git a/parley/tests/snapshots/base_level_alignment_rtl-justify.png b/parley/tests/snapshots/base_level_alignment_rtl-justify.png index c3233b273bc6faa1c6133df8341dd719080ed9ac..ed211a9934529fbf97ddc0f40befbf1b5acc2bc7 100644 GIT binary patch literal 2778 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 delta 1296 zcmV+r1@HRE3Zn{;B!6R2OjJdt$p8QU|L^bb-{0TQ&(FZXz`wu0u&}VDrT?$5ub`ly zpP!$QkdTj$kAQ%He}8{)aBy#LZ(v|xUteE3M*mPyP)|=!KtMo0KR+-qFfT7JARr(g zA0Ge!{}2!m4-XFj000*0TFU?c1bazDK~#9!?b_RJ<2D!s;D63fhABgfip)m-b*MP+ z|CEJH8`%i!#RrkH2PEM?jvP61H(xY7fp#qkhQS^&k z(Lf7y6D%!!rl85#j7omHqP+rew8#xoHwX0nb`%Z$ihnAvaY#KD*Y#M!4ivg{E2?gd zAS^D!($effO(uafp+tB|z^o(oprz#u-;A7Q07?&ZyGvRLz|G`n0W<{QY3me<2tMTK z(=DjAKkHLRx8v=%{8XsC#JD2CxoCiwc-oK3Fn3B1=)%!r$T}_?(J8LEaD@d(1EAU5 z+O?tEaet7QiISGMHJ8>|)3}i29qp>14TF5~q^Szpj{v$mnx-mfyFA;|az_o%=4sfC zijrfZx&1F>=7z39pML!7=+ixY3W-t=RmC-yp)y)hA2rdy#kE5PSi3Zu{ivfu%Rzfy z09p-faEOAxQIe)n|QWJe&&RrKXPx1k^JR6DQc9hq+ z4=7wFM_av6=ZLTJA@ExKI1!5dz|c(LvS3=r^N*0!$Mi3{Zv6 z)Bv4k9T~Qwh8~u*@D!-tr=JBFbt{UoL)Z5$x)FD@4A3r{(RRQPngK)m4a&`^rKK2J z+JA~D@2xeKFNh8OZ8r$_{X`DtiAs z?Lu9QFL>XQnmA3-m0I152KFgm02y*p23XBmL#frx=+xczQP~0wO_k}-gTb090RY=7 zs8vp{rf`6!Dkxx@aWTkObn0rLCK^E4qJO`vK0^&u^2Ojp3&$vSnB|5BTG_Rx%nbma zNi`^FsnxCMm-QhZ2>VN_@6W&g`u$M0_*q)w8hMiXsH6MuQCE)k(fMfZty*gKj-DEr z$IxN!=fwjYsCI4Dak0QTr(`;(4?08sk~+Ld=)P`#992^8E=n0$Wt z8^x1-UeXP9d^Pf_uza=8LqXvgfcV&xj$|(deQ(u<%nh3>if*x%%>(EUFHPk;g>v-i z1NWyTzu>Af1k^`WE1=J}Bfi9Vl7F&KHG9!>?h-q|E@L+@_PS5C`|tWn2Xr$ab602j zcezw)>K=l$b{wMUK|IdbGPd4dM-uPy#yZky82uWv&=HfEOX!T2ZvjRP8p_f&j0`b07*qo IM6N<$f?iB5H2?qr delta 102 zcmV-s0Ga>d4dM-uPy$*1ky82ut>=qwlTZOX0!~|#jsY1K3*_!-`Oze~@qSqyny#cN zYvao5Uy+l&0XG4rv-JVS1Scm)Z{{CBmoL^Xv&=HfEOVdo2grdb-BcP8-~a#s07*qo IM6N<$f?c94g8%>k diff --git a/parley/tests/snapshots/editor_select_hard_line-4.png b/parley/tests/snapshots/editor_select_hard_line-4.png index fda73a7ab7c67e624911b2d21bb54a6d439bdebd..3fc184151469ebef909bd34c71e64427378c1a4a 100644 GIT binary patch delta 102 zcmV-s0Ga>d4dM-uPy#yZky82uWv&=HfEOX!T2ZvjRP8p_f&j0`b07*qo IM6N<$f?iB5H2?qr delta 102 zcmV-s0Ga>d4dM-uPy$*1ky82ut>=qwlTZOX0!~|#jsY1K3*_!-`Oze~@qSqyny#cN zYvao5Uy+l&0XG4rv-JVS1Scm)Z{{CBmoL^Xv&=HfEOVdo2grdb-BcP8-~a#s07*qo IM6N<$f?c94g8%>k diff --git a/parley/tests/snapshots/editor_select_hard_line-5.png b/parley/tests/snapshots/editor_select_hard_line-5.png index 2150a5073289e2a75b8acf4eb85fce03d715bab0..58b5627ca59544c2230726c9755dea45f838362b 100644 GIT binary patch delta 1360 zcmV-W1+V(P3gZfpB!7)iOjJdt$p7!}@893wpP!$AfPjyWk1sDTPft(4z`&4@kf5NT zH8uYL0RPF!|9qCeaBy&kqR*SV?_F?@DKbBOmcUbFfRV4?BP#!cp8v40-=m|?mX^P~ zyzj8EuuxD?U|?WCKtLZKA0QwgZ*Ol95D+jhFkfF^4-XHouYa%4&(D88t>Eas#6wd%0-R;@4XV0EJUsL!ma~m4&FHt;%a%ggCj;8DRK}=kYCVncI zHlrp|8FetEGJobEj0R`36~)Ae(ItA+){UYQ+O!rO^L@&wC7Ehcq1rTk%$XK6l5DzA zYE7R}N$5_hFiI+&5X+(_l5D54Xgvl#p(5Q;ilHh2=t)+C5M(|f+zdUC7nPrwm=<7Rv_sS$`pzSUuEypHE0N* z>(hw1;eYN=_^MEOFlI({j7E_U=CB==vF#H)nFiE>qf7O$679^KGiF#wJCez%rClq; z4F`!9%$dNYxl)-mH3I-#(Y^>;LCcFPwMEdTCfQ$6+Z92l;VB%3E2?l(_v2<%5>Q*H za@lIZhtZhA3uFR##mlM+*}M{!Afg)%yz z4#f*a_WfxQ)O+TnR0_>jG)2*z3ENSxuhTwwYE5&7iA+fu<5!~^LUtV~&`gP`Mbb=N zn}2HPwWxw;00KQ^MwjV3A?vgjW!?vg+ffZgD)Z!Z`8F_bIo4SL&aZiLx0D% zX$MbYL<)1O)8#3krm_XjstYpsxn2u2))*Lf-xO?iRGq_c){JoSQ{MxS&+-bn_F1 zLwFkOf<`7jY|^M7quS_6$_^tn|lz*VR&DElG2!w)y0pL}U5U#g|P zfAifpj)t%TUH__6YnDe-Du6yMM|?5nAbFqq`&RVidW)V6L*2JSxa?E^*oa2N+yQ+{ zO4}Ej{vuaHEVrh<%MB_P!!edY118C9K=Bs31e)u}BeI&^I&DLj_oo_%@JTQr(hiYv z=*cw7Uu#iZ64_rG)Ke?~E2Ibh++HZ^!-4uLtFK&sd-Lo57u`L3_Uzg7K%K)}GjA0HoY zZ*MR#FprOq4-XHYpP$dq&wqb^zrVi#002KfKmYc2(Z0Dk}kgh@m}RCwC$+SzX7 zHW&rq{#>*;>2y!*Xdx*cp7(#mLZXBuGo3*#aZqHcb4B~&gGgBiq~M>( zLYMZUL%vTLwSOd2MJkMkrjI$(ibj%c6H2Y=D=G=iRTV}_=bK{z_@Mt{XUFKC-cMg+Q%w(mUKc0bAy zW=0d1MB0{h>|56U0LoPNGn(QYp|7K3K!7@eDhH6ZVUFNt$u3nqg65V}cvf=hNG2Gv zKU~rYBx46uf#e912BA!$i11y`KHYYZq#=iD&ELfU~$POa@aA|5zMbYRW|uFaLotf{U6U_<*N=m;-wuGAJm z+lpkrp|&Z4PW5BB)ElaBRWHL~R1#2GsP#MV;6}s(efs{>*{4VPl%k~^x}8n}d|@=F zJgT%})N_XlId`d6$I*z$2}5deBYAVSs!bu(lYbtHqYGr)6hhZQYL|f&MUfMWqMeO% zq$K*foQGb_yvRE=;HpGd`%x^3?@&euRH1mG$hki)f*Qk|luDu5in=J8GvPRD{5I{2 zq4(4`=*X0m)@C>AA!O5#0?(9~Iwa53bg6>hiz*lfAkaW&be+BvvPyeVRz3T+o!D30 z(0@GORPRQofrB3h4$~JXcB26Sq7V=YqI9=JR>`p>YB7={>}rh&avb$gq=H#d=`bBe zzfAbQpZ7%-M`Zwtqt-<->Z0iOdpd+htsnThB~`j!9HEL#zZ(?iG?pYW6+uHn^M9l=Mclf9U1GMOoRNj7sZmuerZrcu>h=)2Ke)Mp{UOX>cduFnI!!7>DTiw hx@XRuIdkUw=Wj_X`{$}YU$_7O002ovPDHLkV1obZg$4is diff --git a/parley/tests/snapshots/editor_select_hard_line-6.png b/parley/tests/snapshots/editor_select_hard_line-6.png index d0390b5d69ab54bc500d25303e67608fb2bb1315..6fd31fe49ed186c31e71794c05811c20f1768db3 100644 GIT binary patch delta 1449 zcmV;a1y=gY3-Jq(Brn)dOjJdt$p3$TfA8<_&(F`Vudfde4_{wjFfcIx003`qZy+Eb zA0Hn;KtR6$0Fh8Re=q<5kN^N+006K60PHF?pa1|+00000&+jrazbGi*FE7t4D_wAp zk+0ug`M zNRgBi*K6z~&eDBvlRCB|bHQ?O-v5nih;&?Mn~Fr*Ny2ku$QR!LKczFIMtA1SnKNh3 zoKGqIm)Q)Jm+M%rWIWU>AEK!Wz7gX_qn_^zlF6uXw8pLsX^mM4I)kN|iXx#yt~^t; znaYLssQg$ofA6nTk7gu&OImc3j*pOJKpn}tl(A-dg9?xG!bV0(t36y?)Hsq=5*KZA z$#Zn1Iup@Q>j4xbJ76UG&RDJ^Y((@XpFJus8DcMEq8=SdCWwnRHa^;_v)AakF_zSt zr0zdlBT=6s9ncGvwMdyXq<|+SMx(47u|_o{S(KEJe@qnwJH^qcP-knj_9Pu7RmZHT zH0ydi$_{BjJ=Sr|YBsK@+2{!X(nzQ#H7q+N$-0b$4hvte z7a}T(J}xR+WiCX-L~q79{)?FqaSN5Wu)?(C(P4bIP&x&)GJ2ti`TjHtnx^dIeLOT+ z&O}B-5==)^eSPdzO08*@Fpl0^W75&6h9Jw56wvgZzJk~EW#wDwv8Y8#2}q_80UeI7 ze+1e3u_)VuMO`N>TJi=BUeb(4lbpHka^}ffC`O|a62d}4MntKWKFm)sCYqrmJ7j~^ zL5k_9h9xc10TmUJ$>`gWU;6XDu+dSG1JTh;IntT9=%LY;qh&GNM=42mf3Q)GU*(n|Wk!-nqM*{FJheSX`YZ}c+OBOZ z)fz2}7^pRt6i8SvgAW}86>4d@f~12HGOxsdYDh6@rcfQpQ~};{KtrugnW?bvyd{So1jI1jeFED`)1J)98g+yvGFGgD|y~z1L|;U z#i^&nsXWhnlqodt{JGgqTc8#_e|poZ9iTdNYm`MJU*Ojb(D#06D#jPe-Mfnq2_8(kr_Q8In;}7z~gROWo-F*z;3fZJN)k83f;S!>DEEGP)~P$9E$?|Ecbzvhu94> z1@?Brw(-;SLb?Cu;{LrayHCHJf1o>O&YU@O{)_w#?XjfnzA^6a00000NkvXXu0mjf DzrMaW delta 1411 zcmV-}1$_GP3(E_TBrL#COjJdt$p3$Tf8XEVfPjFnudiQUUoS5&{{R3G5D>t?z>tuT zzW@NBprDacIe(A<05AXmU;qFB0MD=h0H6Q>PyhhmFE7t4EAKKgzbGi|DmB0W0B~?{ zP*6~?u&`iYU_d}XA0Ho2Pfs8qAa8GPFfcHWkB<)z51*f(&(F{A@9)3AzW@LLKR-YJ z|NqD(&D#I~1jb23K~#9!?b_K=+ejD%;8C|E3zLK~TYn%4S!Uk=TWfEp&-=gC(3S|0 zOsKKcnW-c?H?+C1C@Hjui;GG}vP|Nld24u$j#O7R8Y(=1hGYkfME@~@+gK41y~|gR znmvYyy_l#+N0JHRqJ@Z$=IZJ_dM%|OwI-?i57$W4r$`5Mt+E^`lY+GHq`_#ERbjWN zh9R4p28OAEWv4hAwd!h%R-UATq3W15rDj!)M}OHN4XDR9j#q=*GKGm#DAw{p z<~|*R8uM76I=Bf}KfxD;+9y&5L^~%1`9vG{nycmhrGGv1QTQX}& zuYbhQt{vQj2Z<8wlYm2W`8YF`1%UTx69w&{?Y*65QP3(U+1#R89R*E_YqKnFQGvDC zYbT@D19=89{7);W4lY8UK72Lz>6AXDl{OwaTzwko=x9iMR7gQ5Lx);2bg2~6Q3uBa zt*@{lxlvhOq>)ffY6y19kX0QC9TvXYE`MxP6n$J&w9H(HjfvikbNm-GA@%`kuohP4 zr$ znzYdLp1y?F^mXZT=&`6kN&^_CumK&8uPmASu_*I~O;sgqa^6FOmlUJXq-CqSmVa&X z5sJ~Mfq@k;FcDFzjSurvjEQFG$PU?Hbue~1sv$^$bUuYs1s!?TX6DFxL4^88h^183p(P-Ta_fg7_9YmDl zm#tt(oslGxD5&wMFGbIhK8u2qqJPT^LA6EeCI%{`AT1JBo8UvoK&{#cE@9{(t!Ya; zpc=+bnkiIAGF5;V9MDj!qtW~IVR=7hzCRs{BH?36FwA?;#z!68|Bt$Dk5al+irZEl zYIZ=E9mQ^FTUKR}l4#*gXw|$h`=fnv}ckGzTi%qj#;^0jfi{McEX_pxXic z#)e4#vjx_BhdzIyb>(|^x^et+;O@%;QVpa%yL(4WrE4#VHo+1cCG;`!O@*Jr;@ zMSr+BKEm;0aeDIR_{GWT>A}Ut!EY1Mee5q5udrAgzqr}#i{nVgALN%O`|)Oa_!__s zx;nziFZ*%OQ@mRBGgzz+k8mCZeTA^waQxuDIhcmN`zvI1j<=^jpG@4KM>jLwKM5D= z-Rk$TDA3RH7`S+e!!T3eXce}NKc*MT^A8u#pMBeX`se%u-8pmS%$f6FZ{Od^ug>ur5&m7Rpv*3|)TeMJ8 z^0B#CyR_V?x4R|0oTB_M2 zp}*4ZY^}hJuI$iBL{Q4GJ0x0000DNk~Le000140000u2m=5B0HJ-PVv!+Qf3-kteSx>;A>d1}yBlm&r4+)Rgo!|-6r zE|+CR6q52-TwjqX>c|(CJb1(yOUXxny}T_*8>RAX(=sO^=*SnB@{kErA_iYCmv`23 v=|D%mXgNo%Y~>%7EB$5Jemuq)W6aqbf(8nY9g>(E00000NkvXXu0mjfpB`m~ diff --git a/parley/tests/snapshots/full_width_inbox-larger.png b/parley/tests/snapshots/full_width_inbox-larger.png index 00be89b6b4fe7f4985a73b8ad7476822697722d9..010ba063197654194a4e5dc8fa55358fc5e9f98f 100644 GIT binary patch delta 415 zcmV;Q0bu^Z1H=Q6FMsdv@893w&(F`mz`(!1zp${dudlD5prD_hpOBD{kB^UlfPjC0 ze{gVcZ*OmCXlP$wUr$d@KtMo0KR+-qFfT7JARr(gA0H495DyOz0002>Ba|Ef00AgT zL_t(|+U?rQYQr!Ph2f4asq5N`D_g$ly#I4HsC1VuN+CIg^MCILK9E2R=tU`|lu}A5 zbw7JRzti~0q*)PoG#)jZ*@W+gXKJRSKY29!;Lx*ZI(VBG7R|gh5r@X%8e>G_&oku=gsBaJlw>75GkR$$STT1v&@NHoRF zE*OY3Rtk~Eoqw4`vybC-FOKIH7p#Hlq>hD1V-u(}l|-|~7)K-$4FGI|_92`0*5BqSJIGcw%jJh3xy`Z znVCmVp>cJM{&n3Pg~r?rAbXyD%4xfMmPY^4lu}A5r5Kd@p--IW1}#|<{jvZ6002ov JPDHLkV1hkJzM=pC delta 413 zcmV;O0b>5d1HuE4FMj|40PpYbkB^Vf&(CjfZ-0M(ARr*0pPwHeA5TwDudlB_KtR90 zzi4P^kdTnDu&|(@pm1<-5D*Z6fPgPAFEB7LUteDj4-de=z~A5BKR-W(Nw>fN00AaR zL_t(|+U?rOYQ!)QgyD3_@;Y{w@uKbhpEE`wH(x?vrUvT2H-CH}foRa1Qc5YMlv3(C zdqjWIA4-)A%XFg(I$TfSY1KUEO>EO&P zW1!Huc%=M!?2bZXZUPW8&pzd}-91gC4{1s%rIb=iT^9EZhq5g?q!-;-00000NkvXX Hu0mjfBa|Ef00AgTL_t(|+U?rQZiFxp zL{Zw02_fJi2J>?L|958OD7$QCkxVV>?pr#Nk#I}ylu}A5rIb=#XaCSs7$1o+8v+f+ zqh>Rk@zeN1%zO?P4dxgf1`Fvm0000fN00AUPL_t(|+U?p|YQ!)U zMA5vG<+1Gy?LpK2*BRR(e}T{+jSIT&3J%yH73fVVrIb=isaMOM-$Rz1cq|({izOol zGpq4ib)BW|yP3t(Rt_D9<%Vp-$YB}Q)5BnKxTloRFjy*_wG5Vausx64^_CrBM}4^7 z5~7_oc)#3YeaYXEMJ+!(jk_9 z?ief+f|&(m0?W|b5gGyuYCH0t!P3}Ivd~)AU`NYDU}@mYETbc^xVWYKxvh@CVr~Es qGRr=4+G-bA^bkxbrIb=iy;|Syx-5!&-J1CT0000cCO08{`b}1b2`<;=PUHlCjme4&zMDNkQFzABPbQV?7$!Vn YFf5!TDKf!*3eY?TPgg&ebxsLQ0C&A7%>V!Z delta 117 zcmbQkIE_)UGr-TCmrII^fq{Y7)59eQNXG!N1q(BfY+BN!K2cH2+sxC&F{I*FvIOhm z1Q8xng$`k!hEIF~0wPQk-!>gmSmhhQ;_LoAl3i? 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 c32b1b187c8cde9b019dfc085021bca8015e41c4..8f2d35d59bcbea4f183c9fc167213ebbe1cefbb4 100644 GIT binary patch delta 1378 zcmV-o1)chc3+xIZiBL{Q4GJ0x0000DNk~Le0002K0001J2m=5B0C>&1osl6Oe-952 zPft&;udk1fk1sDTUteE9KtQ0NpkQENP*70O(a~^laImnjz`($ekdP1%5Fj8RFfcH0 zZ*LzTAD^F}&(F{A@9#f9KVkSkfdBvm%1J~)RCwC$o9k}tC=`WfvB3D2%gkwfgU$Ot z<_O%_PMfq%BF$0e@JXe_!1gC)fB#tg$X}UECX>lzGMP*!lga!E;CJ8!U-M&q^Y)G? z;u;~~8G^<~RLAk!j+4J%--Qr3g%JEb`l@(BzbnmBX1l|-_v-@yQ-OQ*HJ{LDwU)Wu z@udE~I0*pki^5#!`(j=3K(S+?5HW?sRRF49mZ1tMT>Mvv zecw&()OonTCU`{VGGUm?hAL{LSC`a}>DN)zr$4m)M!y>Y!wQNju56p9X@NeTq(e=Q zG-x`&Py^nc_4^=@FaV&se{7yffGI?c^b%-3+WSc`lxbpC{RA~Ui;umpdw5$9X@5G1 zNzcoUvgvFe#Zb|I*4J@3>mLNbQ~Es0GqEn31lsRCgnDMxZ$n`So+3y}1 z0iMwR{bc=1#JsS+>_{w7K~X&y`q$^r>fea&&S61YB@gSX;$D5_?$Ou0o$}ZD!}^-{ z>g#Zae$3o%iTLgWpep_@`hH3LajpQ9BLAu{r+8vzjr#lIe}u@DNDp%=_8;_B5g(lk znqt=%>c{#qbB~ZAMtl!N2h)u{SQ|WM;@qXkKlQTcOFxYb6=p7?ekgea@F~()fbW{t z4j1|(+Z3Z@hy^PjCL7$H&7-XVVF!$z94GnQJSp-E-J)MpfM(gH*Qh@y_Un`7WVSo;vOy)Njub|Hxjh=?{$uw)#NTa>P5+ zkMtF*_!_^_m##Fa=zNzLAJI>34+v1Pn#L>o!sebRhj+?hjrzCsJsq|LJ#Uqz zAn(;ztOSaMkwbuVEh;J?^deh**ulRYH~N};n4!cwp8ss3zPc6Z+wK?q;5Pc62lZ>9 zL(LP+5CGj7`MJI{3_xYcQgE~al)c2yYO9~3{MeM6-X|FZkxx-y-HP-wl6J&KAI{R& z9GAO9e>!nb<}p_zzW??P;BT*QG2d-7A15XU=B)o;=!XED{7dWG@`d$ZzIh4-Qy7&3 zdu3F}IG^>ie%8lzGMP*!llc?)7b0+QwKi1V(EtDd07*qoM6N<$f?Bb;0{{R3 delta 1406 zcmV-^1%dkP3Wy6KiBL{Q4GJ0x0000DNk~Le0002G0001J2m=5B0C6T7=aC^Df3L5v z4-XG-Z*N~;Ul0%wFE1}(U|^u2piody(b3UxaB#rDz_75eKtMo`kB^X$kT5VXARr(g zA0JOoPoJNk&(F{A@9#f9KXYdRW&i*L=1D|BRCwC$n%j=*I1oi=d)u*dp?hW;NPH>( z|7XsL6Opv&%KDwWDl1b-Sa_?q8VH~+4P zB0hi`_jBFSjC8n=GZxP24)vIGrq4O28`NXGSG{v;Ej4>}7w%Bs^lnhk`Cj$hVL|h% z&qL}j%~=5adT-2LT{X=M0LXbRm}Z)mdb9w}3P7U(^J|xsXWN*0@5`oMe>%=5kMmBT zIkHj6*)+|1yzEi;svn2ojsOm>8+vh;v~xkADS!{#2W@E( z9RcluJ^*aJwYypG=Y>#Tsb>J~qAolLW*$+WSWp*rJFV-M{j1umhYn0#2ioFN z&M)n#H}z$K;bV23*IPWtvpl9g1T`aAYlf{U1wd>^y;F~q^{~C7f51|{QSZN22hz!p zkEw?V8`L;a0851!%XK?-!?6Y2yiSZ|s@Hb;2*47BvM`KasjCL%mWR|64@S;@_ci?| zy-ieTdxbOwAWtGF3{*hJ6M(LNlXI`vc9G}w))NKAM$+izow|{C>V^lAgOi8Up*%UH zi#jyvG$W)V83eG7e+q@xtv@t?w4`eZ@6&x4?t(OFmm0EF^q{ z`MinwdUHVpxJCU(QPnq$JgIt`n0HxFK@&Id$?lvVS62%eFyOVVh2dG&HNFeOY| zri*$QmK0l9f1~>A64k})_XCrFmOxj+dAC!iUiMfTAdJhv+^RmcYt|Njq{4gkoykSL z>lH3orhQb;`D)fJm*5E9U(~numS+_1QRjjIEIWLlu11nObqSmftNj+$KUG%*xUf@C zN0KEZIUZl`Rqxh_j#o}{t#0~Ypmyqj#7v7qT-DRpf9kE8*{S!zT(V#5+WXY=vDg5X z*0XD6@L$|4$rKv{b8D`8&h`&Ky2IeJD2f?coAWX6)@>S>L z=uXtzf2?PcEedN?*VxooGV`X6Q@LYPM|9+gZJBlE1QY)X-bi|2OrVfpdCVbr+sg{iz40Q80~BB{Y|4uAWAn8j9l(Tra?hAy7Y+dIuOr z7@lT*^Gxar$J-PdG5!RG2@Xy7)2Sr8lt6v#G;m(nlb;XN+G` zlVeq{>Q%j}SM{o1)vJ0{uj*C3s{a(~AM-1fN~Kb%R4SE9rScQ`4>lugZ0*``tpET3 M07*qoM6N<$f~6FtTmS$7 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 606ec77b195583b202ec56bf25fd76ba7a18d054..36f8f68913dab84a8469ff114fb0daa449da9bd5 100644 GIT binary patch literal 1516 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 a-{(&m_jc7&hdb>60000tC1mQe-KGT zK~#9!?b+#yqd*vi;W?*vwYw*aTUFivbBF$4!#nl0a#aRN>g5NT?DXn>9xf0-GD4=}H% zxN`025ZMFvX?%r6lW76yX3O7>uIBsqh*HfD3V;gBYv9>@#U;JVH#_6Xx1Q}s+T(K# zYCN3p9ZJsM`G&b;S)bK|dClOvLN*OTrhv@P#jY;R|2*DZ&^2OY-H93rUhB hNs=T#5%c8}(HOwg#yqbh^766_=Ej>r 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 Y7o<8YeNe<4_#DvXziln*%)FES4{Mm#cK`qY delta 3324 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 diff --git a/parley/tests/snapshots/ligatures_ltr-0.png b/parley/tests/snapshots/ligatures_ltr-0.png index c737904ce10bceb030bf32270e80f83d2dab6b2a..18d16f077d5e81d55433f5197c50fab807b6c498 100644 GIT binary patch delta 257 zcmV+c0sj8a0@?y0iBL{Q4GJ0x0000DNk~Le0001t0000u2m=5B02oE!Z;>Hmf5Aya zK~#9!?bFGs!ax*+;nb;4C%Nh!G)YhI|DG4(fD1Q*a0KgH^{#&?PA4KFA|fJPhle6w z>G>F=;o_TQeUq4h)%NjCVp;>dX3C{}g)QcHdz574n7Mg2}jpQ`%VC)k)!1>;M1&07*qoM6N<$f^<`7@&Et; diff --git a/parley/tests/snapshots/ligatures_rtl-0.png b/parley/tests/snapshots/ligatures_rtl-0.png index b6075750d731573182b04ac9b928da7617872d86..457b5fa6904953d7d2ffebb1c7eeea1ba1cddfeb 100644 GIT binary patch delta 264 zcmZ3*w3SJ*Gr-TCmrII^fq{Y7)59eQNcRA-1qU;blyH5JKT%OiasB%B{{H^)@$vKL z&sR`TFfcG!uwX$!L4kvVgTB6gLPEmC7{&U$&l~3h^~~{faSW-r_4e9HE@njm*TB^} z?Q%xT*}wk(Z_Kf-BG6b{ZV(9vH=w{Jc!?O_WO(Fm;OH69bC9XSoxy$%e!rZACYlOcWnsn5q#GXbjI+A%prAtkYxvE@C*Fq@~YZgEd}Cw My85}Sb4q9e0B?hAWdHyG delta 253 zcmV0QbLVgjhLJq5z95-0uLjtWF3a4&d1}fWKTF_M!&5U$J$Xu}7*P z5T?;I1^lLgmH@!q7}!W5nNGuas|lc|9srE()uL(wnAd zOd6T7r7nRG6S~JoOwObW159tYFn|yKACOB5A;fntgsKIhb`kzp00000NkvXXu0mjf D7(rgi diff --git a/parley/tests/snapshots/overflow_alignment_rtl-0.png b/parley/tests/snapshots/overflow_alignment_rtl-0.png index 1bd33ccf93b44f79230b4ed31a48d9d5663b2f25..9fc9a1e060c3f5a78e678c2cc78d38953519ca3c 100644 GIT binary patch literal 2175 zcmai#_dnDR0LD*ep1t>JaD;Fody{e5BRVT5*)oo^GUIEWovhH;nc>VsA<0K(L`i29 zA>y1Z>+3)GKCk!dd7j^%AD<*M6MaSq9|QyfF&Y}^T7W=TfXl8(3%+z%V8!ue_?g|a z(!&j2T&l2(l-aPD*|3mUJ|3CZ8~2hZfR0VJ}7&74-RuGF;IU z1jN7iiohev%svBe|4-qlROP<@5VqQK0km~=`aG$h^+3rJZFXc%=obRQ5Bobi5G-Ipy+L#t*p7KGNl(Qg2vJi2JiGx%Zu(XT=M~o zjcb_6C4k7>q2jfrus4<0TcB%iueWdDTO0Xeaz@et@tC&S{$(_+TeTH|1UDt#_#$pI z^T^N8WKlO=eZybbG~pU|cComd;!YscG}033H%ADvd&A^cePFgp zBrERuq1a@Is>!;Z3+^Ks4=`6TK4p<219|#;jeaW_=_3k$36+W-`8``|VHFYL^N8;z_KQ$w^#u6mj zR&f$j!bF~A|Kl&H^CqCObL{z<;hJ3AZz5WhN)i|`1FO&x3g1M{8M1K_ zQ;_5{8G(B$t2<@)g~IMJl?6o6eUPDR9m_0+ZO@wjc_-3A{a!x3+0wK4CBi#FHfM|+ zGBL-(MdMPFRij|<{{r{QeVr>=z%7E)RAo@I^!8{waNBz8=kqfyZNrTAdy}7Lf93N+ zh-Pyztu+{_2iM5zXt4E5)x>>I7aU17lM5(Wmk?*E)?&I!6N0`j)!1Q(EuJG*cdiXS z`UUSf?GVI**RwPZ2s`abB5pw>(4SBNZMZUW${;w5D(DZMf5bM-n!-~I-(LcWRyrCwX{*H%2mht65qthgzqTQT|5Y9(1+ac7>C{_=T|XU6Uj4~6sL&ry4c)D z^tQ4{>`khGE2onv;kPaiYaOtb4~p!Jv|;Ma4&D1R_m6r}%jb9?4V)2@Z%BKn8r)J$ zEw4da5&sP6E;1r%pStB1OMMOpR}>|a?CHq9N=G~~3ey1XpHTbIH-|jjzKO7uQCqvf zoG$1Iq+!#)aWZwmL#4K5y*_dko)900?t=0+4c{JqF?9Eu(3A>xmsu^Cq=s`{@-GAr zO7r}7App=_^YpPedMpjM(M!3Z@43QEH9oPGZ!qbuADs_)UL(B*q_)EU)T&`WLsRA( z00v$Vhjb^-ph4AlZX-OkX!BzJuaYu`+j^_jcbbNI z8*ZxJFRjJc(qtc~t@PO(dKux~d+y`rM#YlM0izcq#7%KQVQB*-oipGZx;T|ocP}U55U4Czq(dX>Ll!5v*^DUseB90 z*`BuD0Z}{dgz_6v=u0S<-@y*k!B!j_wY!sB{mds;BId81k@aXkYfjeOL;7gQ`SnywNOzb1Y zh}3N}Asl;A=*g9Mr_`|bc?$c>bVf%#1Zb~THpdIWg&y@39FNy5AB7$rwER}dil{y! zar7?RT6P<6w89x16lr8Bd(^5pNy_oZKo%a`ePQIoE|#9hmW?zGELl!{kT*_YgcmnU zRBn}TXJl=&lb^3jTicO(iyAl=hFUpPEX0%K+$|>I0({G1>7#{~a{T4HgADadbnCU9WB&uQml(SM literal 345 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 TG7d@A00000NkvXXu0mjfd#(AH diff --git a/parley/tests/snapshots/overflow_wrap_second_half-0.png b/parley/tests/snapshots/overflow_wrap_second_half-0.png index 16e45d0b71b693c8695c54f8be9295d56a2f332d..3085b78b4456facb901d74b681750339ce46a102 100644 GIT binary patch literal 2906 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 G#r+3Pnj`iA diff --git a/parley/tests/snapshots/trailing_whitespace-0.png b/parley/tests/snapshots/trailing_whitespace-0.png index 4a6eaea3d93be36f9d388306cddd2c93c6f9b26d..77bb3248f0329c1233dc5fa96ff4619df00961f6 100644 GIT binary patch delta 401 zcmV;C0dD@~1L*@HiBL{Q4GJ0x0000DNk~Le000100000;2m=5B0I*|8Es-H(e^5z8 zK~#9!?Uu`KgD?~Yd$_hSgrqIT@BjbL5*v~#sVarLisV_1Sd7pCS^j7=8jVKd-N7ro zR8;~yLhY6_vpUdTh@`RMNwKPmm>CF4@T5;s$Dq&0{$>wwRWm4H#3%5XHBEVW;AW4 zSnujjr;G0rs*S&m_q942GB)ee-M9;w9-GO#ad(9yJeekTRo+@=w|-$%2O-3XCOq1O v^$*a>5`fL(Ze46A;S=p*-e@!$jd$kFce_M^AMvTMGq=7IePAJhy2tf+~J@5891ITm`;*BCF z6sX{VG*^v)t2S~%-eM=yidc8fW`WrOC@-E4oeO7-#xxWQq$1^_(mfy25`~4{DYlgl zc0)mX{MMoEp&ewglL0&!Gc?FykTfRT8PUiL4ds*td|~`hc~;x5W-LFHV`hzJv@}!P zFY<@O$qxzD=38S=_CQlcvoYNm|JTfjX7bZ4KLkg3Fik%R$?I@)v2o*tCLzRu7Tnv3 ryK1zv1)y2H*bw_Q(&=Hpe}hRx zK~#9!?bhpx;y@5UVNX|Ia#45J)m*y!{hxC;MM4;vfdPXsP#-8MEb!3zk(`V%#u#Ia zF{cx6@LG(E5FOvQZ^!UTOsy=OIM{dr@qBO}0A?8hYT|=^TiiQhat|=b89@9SV*bQ< zg+1n~vl?}wKeu{4fs650AjA@9f88#(cs(%Y(sxt413Jb0`p-pK`wf+#RT~c zQ39w*+79S?2~e7g{`DR+%1o$-#o+EDX=aPTH6o2;(@4;!yQU?PrelvGFUzmRJSk6O v;cJB_flvL=r}i7zi<&XU8yI7ZF{jf%$R!s(o{l9I00000NkvXXu0mjfVZ+Y* delta 444 zcmV;t0Ym-Z-{JeM39+0J98R>dV`|t-Zm~ zzLmLH%0fT1<-CHj_8!65Xnom^e|LMk-X0sRoAGG>{5L-)C?votYtDuyKS8@TqGWIZ z(Zts2Fh19w{$zJiEMmZj+S^{%JbQ|H0_|`ev;#&Kh)u|C%C?pW`nk4j6u|*Fy|uSF z%@ke?lP09xEh{i5l&I1sB|u%h^CU&u+vU~1fDs?d<=`c5ZQY;R@oIZce-!_3Ar(b3^2_{du{=LwQXc9hrMm&7uXyrR%tt+ z>%~B9#!@=ki+D`B+D1J@oM&&N8j(h^YjTuT9ssKoX*!NJAum7GK8dF>^1jH4A$!g5 m*yq(Bld7t!s;a8j^A}(%5*9AWLH*wV0000Hpe~(E- zK~#9!?bh3ln=llGVU`y*#*j?g;pD^nKWD>M#v-bki-txTX@6G%d5d4MENGNcN-3q3 zQcZ(5crC`{0+aFFpCh8=^6xR0*tIm+)r*#R#0zc$djN$2)#JROc|(k40x(EM&+h-n zn4EteW73qf0Yt|YaI^ye&mxPbe*~0l)?8%143 z8F=D`zQ)i3@>Jnjz!sxL&lL*`0DE60Bl4p=X`3vC{fcpv3Yk(bSlhQ*xJ>>fx3yJJ12?wmAJ@e~4K)m0$BoF)%YZ_NXyvNsbP8j&yQ!maCDB z9S$+c?&IuEdp9LKDtID2C%sdB)3^2;=!cq8N-3q3Qcd$4y%!ZaY4$jB00000NkvXX Hu0mjfm-y2u delta 457 zcmV;)0XF`t1fv8YiBL{Q4GJ0x0000DNk~Le0001e0000u2m=5B0O!jG$dMste}_p# zK~#9!?bh3h+b|S`;jXBw*p&+Y^hIqycZhCe(!5-D~yindzHw*xV((Ty3p)N80 zI@ZG!qhQx^1uV?~z%iB0Qvgzwe>J8upJJ5r^-uQBf1uXw=AGdu3d^N@1zKR6^Je4x zkPzrNOCGVq+>akG)?;?1?B^-$YW>P^IMaCpj0tR!xIowdiC2KM`>vh|FfZ2c1dh0& zt92TnWD3UwHW&>$u2^ykX5Urn`-k@I$r~2tQMY@SJ&Ki`P_Ph=rTeHif5*C z107I~Im^q&147w)p#P%Yovk;t0Bo(pUjVH2cA3re0O;7>uK+fGsQV4Ta<-oQ#bTn= zdc!H$ix2hAHsf09W= zK~#9!?Uvha>mU?FJq#DyNlts(#5M;0|2J!>8&DsrDorX?+DqbP2E;N(vO7T#1VIo4 zK@hqQzrfFAlCgj>5#Ob*Dp-?llOYv0G!?EWYJf~YOHTqwz zXpUqm^(9A#+hv~3#)%4+Yk;S%GAbtvhk#7tfSG=wg5Sn+Ej78MkXJ?0!t!RBowCEr z`Fy0VGLB1oKgfCElBjWF+TZNT9-8BRaF#28ADqmXHu8GSf~zvyXz!z@e;ql444eS6 z-^)CTOdfZ-zaXhiir3cm$J1!5AxzGdgJXaad5s2iBLZZu!Vm7E{ zBh!-sVYq`#B>(+)?5j-LlB1)?&QxY67QOjeRvAomM>pL;25BV^7FDJsQ~^7_gn^uZ zy}SgpGV`=W{#Kb!1bimqDhuLwqQCmpemwG{CJ2Hc2!bF8UH1khVi_IOV*zLY0000< KMNUMnLSTZ+4bE)< delta 463 zcmV;=0Wkit1gr!hiBL{Q4GJ0x0000DNk~Le0001i0000u2m=5B0PVYL-;p6{e~n2* zK~#9!?Uvha;~*48XR`oblC+&p;v44we{*Vt2DdL$r;XGH){CXHId~b6%&$tNQmIrb zl}hEa=uv#lAUZEOx9R}7E8$%{U;svvY++MO;UqVp>!!v_7H=RDh3** z^dSb(x#zt>s+`x|#Il@_BJ4u%f7aHmsY}n!t@j(evYdF?8~g$2+TBNX%S_aSRSvym z=^>{026U%`1~2Z73r3zLXvP-oS=95cwIXQyhNTw($O{O;~QI(CD9ZEi2GA&@jas;e=tbUDusD~ z$?jh`iIyZ2i@}gya=|NM=r5$P%+iv|4b%4-+?b{HIu?VOURq&PBGPP$A?;2Hi$M!P zp}W^$g{R*QZ83=ObZ(fH8Pe!TavC9r#Xtkixf$*?P+Z{TRSZ%E6O@@#F~A_@@D|<~ zj04=}Lk1rR`pBxnf5TGxC7J(5Q|}^`N~Kb%R4SDp$zMRM7#4$qKPdnJ002ovPDHLk FV1o3*)EWQ) 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 3ed5faea382a81c968cc29028d82199727d49be7..5563a5fce317461cd4611d15b75d9188dd62baa9 100644 GIT binary patch delta 2564 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 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 14a9cc3ead05eeaffdaac4fe63f52fbd80241f4a..30caadec8deb9c780afe3b7f320d30899512361e 100644 GIT binary patch delta 2390 zcmV-c390tR6U7rDiBL{Q4GJ0x0000DNk~Le0001>0001p2m=5B09Wo1M3Etie+5ZI zK~#9!?VIaz@))W~Te)`SOtg-Nxz7B1v6IvH&vcAfW2w z(xU>p^l77}Lj@eVuEW>~e@K@B`$s^)E@h>4y+M6~9F6E8Ai8NDQL{x_v<|6P0plE2 zn4lM6p#m@unN4v4FlyM=mR??um`Y0DL39+*kv0Q1;RUZ@$jj-*2=H2V8ukEs3WUEn zd!@;M=)$YJQQpKwE^-hJ^Q(Jf{heE7uia5VY#PX|hUBOKr^#MYe~%=xwoYJtg4Lv7 zQP%80Zk-04C;Nybw~>RM%PUxVa!^MA@hJPyPzrlesH$_6-I-C-9RU3D)AetNp4o2w zQ318a?@PUl)Q92S(9I!BH`4jufTl&2;if&qiYnV$6h)4>Q;Y!m4i%8g_Xccu2WSAL z!^2R~BPl<8kR^p?_Fi3+JKlq@W z?tASZNh7zxhqQBk7H}T(eRc1EZU!!10mH2F$m1)%w(X^le-$t@XN3hTEdUOH<&s~q zFj--9{Sm`@#0~gU<=b2DNc>cY$97Xxa|}f4O`f(6-+ju;B-Q$LfcM2LWqM z??pUZ{%HU)f~Hpa*qG7)B#@?+fJ@Fgl>kc?o72W-)wY;{9w~7 zKwok(e{Cd?w~4K6ubYyJM*++mM;8EUK*@jv*3|18sL%k_W=_&6b%|{_#p=0>~G83rz*&P1&#gM}W}t zD@-t`fQ$-5Na+swLGf%gH zPmQLy!|Vt9(DuW|SnH)%iC&R(cigNef3blESh)@Tq>(hTll}XXfL;NHz6%X zQ`}*(eZ5K`1yBj>LR$dy$+5lw+DncBmY!1RF9i!q3Z^i8&$^v+d%RG5HY#KJ3v;z)pKfvvEbNTszwF0sDSQ5@-Se-fAzng zU~z~0fD&E&q$;41WOsnB0;(|B<4-VW8Sep$J9PViu2qSr09wX0K}oQTRKPS-9R|!^ zZMp-j?r;}SR;aqt7C?;Q6|Z4L0ZeDf_Qnk8cr~vR<3T{BN!{^r4OZK?zXs`703|q* z98m#t)-3}{vVP%scqdT`JFQ`dN-fcrDZm;5 z#1+8JWXmbhR(&3@y1&Rx_dx^o#MjE`FmOcv0^n?cE42;+IOPJNsMHc=e@bSK1TfEl zV$7D)1>H!-PXiYBr@{c(z5q%m*{vn}r_SeTMg|OWN|i0A z&XvOhfIGgU9tSQ=y#-u^V@@cgKzP3f4T~8RZ6n5ETQF`iyDss=fP%6E*f6QF%tEJu z`vPGB6twDX#tCy=rgFkre=CiL0avO$z+gmHu`wbv6L4Q3tWVHYK01J;fYl5p^_KzI2A}ejkLfrdpsRDMfbf&SYp_^&OJ4v~ z{p8Zg@5&qOS6=|F66=STFe0nt0xYFsmrsnp@)%%sZo~EnXf^30f9e+iohQe>0h*rF z4oS9`xg*&ZzWnSh*8(Wlb$Wv6uph*(>_b@9K$U$Q+0XO*=f(d5&hz3E@C5v>fTi&F zyny*2czt5PO^J(pTEKFQsh%6q&^$Y!dv3sncwWHps$N0|#Q3Cu!*PMtb+>eQ)Ir+$Y118#l>gqw>px&QzG07*qo IM6N<$g3t?t{Qv*} delta 2391 zcmV-d38?nP6UGxEiBL{Q4GJ0x0000DNk~Le0001<0001p2m=5B03p<(&ygXDe+EfJ zK~#9!?VIaz>nak32kpfIfk2X-WbQL3u_f{TPdL?3loXF$XQHz&t|_e_4$!#$1yNyD zW^bf()?2y$_bwf6H?VQR@KJS_vo_Z(kv8cTIV&5j+`wfv9AX>1fSM z3#b>#y8};mBLJK_tpOK&=YsZlf3DS zEEkq0XJVj+8YQpYgJ-2skieYnF2^z3uQaFXZ*!w$ene*;R^J`BK2 z15v-A{7zgH(0K(c-5xyn4(PPB9A}hI=uh_Z-kYfq5*0XO@IZcxp+SCA*?WGp*h*5C z1VLV!unR8*rI?`0#{r54PV;oJ8$84`d-&N<$;|v|Wxl)=JhyOsx5!c#lB{?v+J&cL znj%&_mtSqtbQ`?B>pBdbe_(V4FW%w-cB(t9i)OQW9E@le9=h=~Y;q-9v<}kS;H6WT zVS=83i3-4}FJy`nfI-8yw)VWUs!Chg;d3hElkvb#yyN*}F4px-a8>`|0{OfM+&T z|BrZD2NbtHTn{HkW@Ece}A~uqiLVbm67O9Sw84wMM+2 zZBM7x7QZb6g4dd_;UyN2rcb_K*Z@`#?9tXE4dqC;G-c!#Z_(O^yLbgn5I~cuR=h+7K<~g4WA|=R@#@EMx{qh_ zkiCc3pFTc5&Zm2L4Y3msu+tJODxUp)Jj>}0o@J5wo_NKhu|dTvf2I=MV?4vT49_cG zHG%Nm9lSDWfBZUL^=K>J6t3zu=$yL{DVVn6T|YivPhKJM-M$6_j+27u)Irn)wZR*e z$_+n$xC{5~J;n(uUbbePs(5OlMi(m{N#9uj9U1fVp1Oo)h%D2JH%FS>unXBOJMdPI z_Qm*B*ZF+3?tG8dtXs{2Dyde?G+H^mRPjeqX$X5AgQY{|o`_ z_j|yLc(n4<0AlpaTBT!SY6Fl!o>l`cyXIWNt66MGo07e@GtgexrkZYt-&14kmf(Q5 z*!656MlaY;lG;VZD~CiE0BS(VfCT2;iw#t0wl;E24MV>}~rH+TbQ3GeZHX8 zTdQ^Bx+N-LsyX1=)|muR@s?l1<4Elc0ULayX|jXs zeQq?>{T06#LmT^rX{~i$Bzi&Cr|V!ne~QgBz|5tOqej-iZiwfv;duoZ)+<+CtX=VV zO?7|O)c+D5^s#8KyQ_YDkjq3i^K{R1q zz4)e3wYd(q(CblG8mso=wSnXf9#V0CTRg6Kv**|fVTG}!#Js_?8$5R=xf8G9fBfI8 zSKZ%zJc&+zRKRwv%qKjz;;GQvy{lJp44?6;`*Zj4T&pro#j{L#f|6hzS@H5rwG*#+ zvGE44y1%=4vNP3%w&Gz3uXqguDqgy50H(Z1(^@&&w;1uoP&2;Nuz#N4atE(rR%2O& zP6OKlU&X6n)xQiQPH7tJe=TIK6z;~GsqWzgBeIH(5uq7@ZGmsQdTzy&9+jnIwG+K} ze`NI>6M*psFVP|^cF3A&QEJK8jJ4veW-lwAdtm8(&Qo8;-FVQ|nQiglM}ya3G4Yze zfT!Z<^3CkbE9{%EcvgwE!%G-Y)Nul)T5~Hmx^LW%r!?6ww|G{Qe?D!#fag3p#0F?0 zsU5Oxl(_>LD_vQ4%emso`#L>h|EZyHSN0~XZUVKvjy%fq{P(&40*?FK1Mk55!+5oD z_pEs3qIP~-yhVvids4i5)u^5s&(J(Ko_l7zhIm%I@Ty)y2c-0rc>Pk0oYznKaMkmk z8sGSj)r|e^8CUOVEzexhU;lFalk%ZMhYlS&bm-8bLx=u|{sVyM1B8s`8ms^S002ov JPDHLkV1gbZs7?R? 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 bcf9920cd34318adcb0a0f2df074b78772d1df10..19a0fb356dfbc692531d9f1e29d09c7b10d4f95b 100644 GIT binary patch delta 2892 zcmV-S3$yg#6w?-vB!Br(OjJdt$p7gwKhk7>?j*3KdC2K~#9!?AOtbqc#+P;oh!PwHMpzMWuGOt!9YSSC|1~ za{>Y5!};I;VOmV0A!(#uRLjn;_%0A2WBDht6u#T-cDvnfx7+P@yWMWLdk^?RK5LVE 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 FV1j7^Fr@$h 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 b4b2660b916ad55a7d749a740f8eab39a509b4b3..4b9942d61e035e364ac02e88af79c024e1f890da 100644 GIT binary patch delta 2899 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 delta 2719 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 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 fcdeacfdbcbc8114c940d4473738eee035b69c4d..f1e3d54329b546c11e13b82461f78fce9c1d12c6 100644 GIT binary patch literal 269 zcmV+o0rLKdP)m9 zX8f8t@6)FlTY+Y5dB!~R?DQ~(;Zb0$(J-tLUSd3wi9V<>zV~EUmlDIqhoQtQ&64RO zO3WaVX_S~-5Iq@eDKMKu7^~Nw-~Bj_-FW1gXZT8t_C|Z7z0uxe`nV~j)H!ujP)TerFo-URaMo>ICt6k zJ956qt~(h(7xp+U;U0#m&;-|vlTJsFj(X0yhEr>#oF5jBvE>{)KS$1OUBX$@oNH*f zPR`vGwQ$Imal3;G!;|m+oO2$+D^9|3YPqZZ)&6RK|JzZ0>{V5*>;q8T2X7Maav}f# N002ovPDHLkV1f)ncL)Fg diff --git a/parley/tests/snapshots/word_break_keep_all-japanese.png b/parley/tests/snapshots/word_break_keep_all-japanese.png index 5718eb56f326ac36974edabb74055ca009c11eff..ff9b31a5a9192bbc8217a64d6abf825cb7b5d6a0 100644 GIT binary patch delta 413 zcmV;O0b>5^1MdSNiBL{Q4GJ0x0000DNk~Le0001F0000;2m=5B05?*gE0G~xe_TmK zK~#9!?U&1PgD?z5do9b5zS0IvrT_oYBGIJNm0O%n7SY*UBhUD-0PgS*1VIo4!To>^ zel6h-gW)0PSotxjs2)2dIsnLu(IVjlfD#N}7MvI;1114<+$)&{LW-ndN*Ic}jDcX& zIr}7e?3HLnhGGq3KdXqJWlODUe<5lc%+w)aG6_jSLMoE+B{9DuN%0M)jQtWmh)FP1 z*lB~FC78)1$OP9=hXf-w2{s9TOL&uPb}Ai`n$;wd{DwKxMUt~ik}2$}Ly|xy$#4mA zI!lPjBt?8ym1MW15J+62)$dx$<>?Yqn=ttXXt!i!fD{A!d+;W(^Ngu(a~s7cGxfYD zPs_3#6wek3?`u3J7{1JSS>tuWbX=C>v{QmV5t7^WNrUV4Nt^yYY17{)ZTkD9O@E&> zxL%*M>F<*^{e9A=zfaoq_eq=nK8dgJe@NoTD+q!h2=0eZ{Usi=x87!%00000NkvXX Hu0mjfltH!@ delta 409 zcmV;K0cQU11M34JiBL{Q4GJ0x0000DNk~Le000120000;2m=5B0JN5w5RoBWe^^OG zK~#9!?Uvn6!!QhmIdPnH{0-Ko*zf$s*tafC3CZCR`Xu8M6R7HFy_+kRm9U0&;S_kqI_k zqECWHgI6OW6k8Dc-;=o4j#|`0f7BMt)aGR}FG)Z^ND}eoRgF*tNxoo;X!7z&%!^!L zf~4%JS?yPwH-O9=;a=i! zc!|lpNqkn6q{W*EB<|JX&nD@5AB5Bh?gy1XUykyJE9Z?{ad5Ae{1yiZ;jsmt5^1MdSNiBL{Q4GJ0x0000DNk~Le0001F0000;2m=5B05?*gE0G~xe_TmK zK~#9!?U&1PgD?z5do9b5zS0IvrT_oYBGIJNm0O%n7SY*UBhUD-0PgS*1VIo4!To>^ zel6h-gW)0PSotxjs2)2dIsnLu(IVjlfD#N}7MvI;1114<+$)&{LW-ndN*Ic}jDcX& zIr}7e?3HLnhGGq3KdXqJWlODUe<5lc%+w)aG6_jSLMoE+B{9DuN%0M)jQtWmh)FP1 z*lB~FC78)1$OP9=hXf-w2{s9TOL&uPb}Ai`n$;wd{DwKxMUt~ik}2$}Ly|xy$#4mA zI!lPjBt?8ym1MW15J+62)$dx$<>?Yqn=ttXXt!i!fD{A!d+;W(^Ngu(a~s7cGxfYD zPs_3#6wek3?`u3J7{1JSS>tuWbX=C>v{QmV5t7^WNrUV4Nt^yYY17{)ZTkD9O@E&> zxL%*M>F<*^{e9A=zfaoq_eq=nK8dgJe@NoTD+q!h2=0eZ{Usi=x87!%00000NkvXX Hu0mjfltH!@ delta 409 zcmV;K0cQU11M34JiBL{Q4GJ0x0000DNk~Le000120000;2m=5B0JN5w5RoBWe^^OG zK~#9!?Uvn6!!QhmIdPnH{0-Ko*zf$s*tafC3CZCR`Xu8M6R7HFy_+kRm9U0&;S_kqI_k zqECWHgI6OW6k8Dc-;=o4j#|`0f7BMt)aGR}FG)Z^ND}eoRgF*tNxoo;X!7z&%!^!L zf~4%JS?yPwH-O9=;a=i! zc!|lpNqkn6q{W*EB<|JX&nD@5AB5Bh?gy1XUykyJE9Z?{ad5Ae{1yiZ;jsmtFE1}JFfdS1P#_>6A0HoYZ*N~; zU$3vP@9*#5-`}5~pO24^e}8}f|Nojj{Dc4i0gg#TK~#9!?U+k$8!-?C(;3p=#MTV^ z_wIkukZ&bqV*{dhU7JF4!x8~O4gD&Lq9}@@D2hkGkMZpU{k>37_Mh5PyVC@AuN?_; z0SG}~O5CP=N~au`0dk>FwT>=;-PSquhMYp*l5iT#1u0NZT!SU2vrB?gCXiE{csNc@ z?pqQh5a4aDyk;0L72%E=3Zi#i9#ZnV?nR8NS9>Ogl2$@9&Lx!Z%On zn+FAVCS-8Rynh8M;h~!n61*+IyIqsASclR$+ZZ;b-XtL!=E>Z#Ct*0;m>`WG*Tz&p z{r*cy1>gyBw-!GsHvfOK|QZb|ri9LI~`b1CsY z^^tg;dJ6QVXFR_h$J^A?9N!U3ugH(7&m9SPj7s=|+81hHsC}XKh1wTtU#NYdeyqQu j{x~U$q9}@@C?4S-CQUF&;eFl>?bR;4UrTSu%`u^ zb!Tf4E_zpJkC-s0NGi47QDjRLHbv%&+0)BJ724fch4Cg(%oTzn>spsBQ9L;Wf~!KI zxP(yJu|&{?G9fT!)h}#`(6B^UqJJRHWiLzH`@#~{lT~brY7ssqH9n<6z>Fd-tW@uR z5uvaWTOtK}7SLd!gmaIivi9DcTUA9NQQUY@^c_(*m{TN`x9**L5_bRhCJN7(Sg`62 z-4V3_OmGF8{dOLiCOk)Ge@~U2OLRdLkVd=fhUnLE98ZGx3B}vMtKwzgp-7-jJmT^B zIGzU{M)`&^@dAAvc*l3WR8y4mDCbemqnt-Mk8&P;i6}q(GMP*!lgWHGe*l002k`0{{R3Mg|V>00015P)t-srpW*Q z|NpK#z})aBy%?P*AY2 zurDt!Pft&vpr9ZiAYWf!4-XG-Z*QNUpWol#fPjEMKR<^|NKpU)0s2WqK~#9!?bzLF z<1i40;r%p{{L>`eZGLP^GxvX+TMq&!jWG*xSrz6zvIO~fz$YfZEX%Si%d#xXvT6(e zsecE?h?N2dJmd*qY8pI7?s;Sfpa{Oi4shf)fGz&T1XE+X5?(TuP<1PSvy0Jq_06WJV?@d=`2PCFL8A+kzZWrKToCcbZgnH3e z?5ULt4GXX#@%=GF`cabpklJ2)$_ucaM?5B>^D-hdybjbgWq>@&c2jN&Py`60FRxgE z5}-E)*vumn>7k62o7aJhasi^;6rd)Td!R2j1*o}4px(qCfX$7FHi6+lpUD$Y7NCx- z0Nv}rNIt-A0#AJeWDDqYa+)NBHk({2FE$j-p_Z7wA0;`OM&Ls$p)?OXgqG9PkS z1t#j;=ST$Tci0O^n!`jizEgwwVI3HIqT$>Ec_hCE)Zg~LjO+xwD+5{xzRLPaHsbFn(1P_z?1-xuNPPj^#LPu2V}n8ID;KB=MUyk4KUF#jV+>#qePI0qxO6m zkD(SIc_5O59-zQMZz=(5s$76hA8G-{6A{%6JW2#A1}Z013=DLLZ><8`5Gn*BjYRL` z05n9dHXyWsQns&|9|E*LJ}t|#EX%Si%c`xv$1yB==hWu<00000NkvXXu0mjfWK$pH literal 663 zcmV;I0%-k-P)(l_8~7>DtgG`o-x35cKq<#{E{ua z{YCWse*X5?ZbKsP0WfSwG#*wLIn^4ldby~D3ar`>RkNPRvu%bBE)GL&YSUyjQHtSFBzdxu$el=TN_345*pf&nfZ3AB@Zfe`5_QIGK?FaLpF;~& xq^vHwXOOey?O#t+{unEjN~Kb%R4SF;(|<5$JkVfrk-`7~002ovPDHLkV1mxZFV6q~ diff --git a/parley/tests/snapshots/word_break_wpt007-0.png b/parley/tests/snapshots/word_break_wpt007-0.png index 32f923f1a22d3b46a62e37ccaeb012030c31b68d..1d3bfd6d77517dd0effa35b6bcee0e82757ed6d2 100644 GIT binary patch delta 1090 zcmV-I1ikzD2iFLYB!A~nOjJdt$p8QE@BaV*|G&Tg-{1d#fB&DK|35$fkB|Ql5dUv) z|4&cl>u(1C?KpPQjPp{XnKc7F4$B=-(fFF+^5Qq@p@88eo z&tI=!FPATGw{QPnU;iH;|F5tAfPlZ>zwh_&|Ig2VzkdLL063gD|NsA63MWZhzZ45QW7dDN5AhEpD2$>3a9Aw|iPUNh?_~qn`JFo|=@E)^!6^1PT-= z&mVp8fp{<@fP--q6ciK`6cqGx3w{bI-#tY%&4d8}Z<>*PvQg7;3u4kVI}Un>9UUM4 zG9)D?rRy;RrbSg1Q-I~QRP^|9mj?yO4wvC*FZ1IJqdNU1#&=ty=?9#EdqQz-nxP z15IiHU{m{4fSGTdc-%X8h-gSkUK&@6AT3-i3B?SOfUGvI7M5nh)v_YlIl8z%tQJNw zTuMT$>wkV$0?@Q=W3F5@7;(t)wqs*Gv|~kXZqG=<&_CuNw`ZjXW|9(6=eNy_6bw!I z#)*;8#Eh(yjemQ1Jg8&5CemdFNTMR41WXZ?6$zM)Q>h$?3+rg0BB~SO5GB#wyLSep z01Tb>L7;~(ox^Zje&>|-3Qpe$A!YO8<=eMsz<&VnNqM?>wOGE!XYgWix>%ln6_yS> zQr@kB(-&~CK7IKmEOr4FJip89cHILy_^*d&dvMiyrd+m{H^X@kXFaTaQ_gQV)0Vkk z-SCw6T@$wOd<*aQ;NlZ_q_l6&|6g8U7oIGy9Gt!B;K^suUisAWu5Dkff%akp7yAI) zsDB=wS@63p*RUD@gHt~E=G=BhUF_(z3FP z49knEq!@@vRmIF2bycSZm=;+^i8aaxCx0D!&6lwPSY1Jq0Avb55-=2FUNS&sh=~IU z36@rf4o`mdnJ;cj0*~!^fRHR91|a63#D5wIvJwEK;A9X*_wM`5m+J+I!`% z1F($f>f{Sj~e@RwguP*6}%P*4Az)B!8<=OjJdt$p8QU|2UjD0Du60zkmPF&+qr||A2tM-@pH_um4|P z{~sU!kdSY;Z!ecGU$0-!=g;5o-w=opACDh^zkra(kUyV4uh*|2kRVT|PY;I=K%hX6 z$B!_WFrd$%u-CAk&!7Kra6O(qRH{_}z`*}NK>tus|FE$CV1Ho$ARzysp#KjK|1dEB zFE9U3PycUk{}2%WkB|SKpZ`BU|9^k~-{1egzyAOL|L^a7-rTeR00Rk0L_t(|+U=No zkJC65#rJILW|9Eb@=cEjzRdrxsFUwNy z0j?{hsQ^T$P1RXIQK{skccf~m_fBIP5;Iad1u*px{@{Tu3^7pFfYty@6I>MJf(z9F zZ3Cbx=ROxQ2y)~`Fod8G74(cVBgJls$fpzs4-UvX4}Spf=_Cz^$FnmWCa3exjFh?& z;Vzbx4R`5jU>E}0N`!yRU2DT#3NoXkWHIk9OanqnPaLK{4qefCjI|EYU~({r^TZBn z?!;s*K5ofC(BEQL`?I>8Sak$c^+&NK4MBT%at8FY5=ZZ}hn>i0P0Uq9s%Zg3Bzx+? zny4wsLVui`QrEyBy`zAVsOrhVHbiaP%}4Q zbq~ydn3O*@?>6f<_ypc=9yaT{XJH*6BIV^0czBBd%ZJ}Ths`y>L*#RLlPxE}2!BoR z{s}Cy$dsGx=F4z5!Tkiw*p$2H&kWamdb{~4FMq}%oZ+`Kym$h)|3gGd_WJJ6^)0U9 z!+H_m{`CkSK7nizbC;J{_HGGewJ?U$4iv0p-6 zS@F502mlaMp_zKjF4UK?@}A_h?~SIEHAn52 zgMS9NDq+Y1rh_E|2paI%FrZru&E zj)XUgnEi4oNMx?-(8bXDVnpwk%VCYWK8&fjPAQ1v_e&@;Mxm|Y>z8$80c;0IA3L;T zeX);XoUJlZ`$cs-9J?Nla<;Vwnp$v90$ip$!gU5#?ycQc3|%{{$VKfJC?aV96p~0Y zLXoM8FgqdzghHbQz%7v+qW6o%DE`t9e1W+A^3RX^=1(8~l9G~=l9GOI{Rc~dfOKOa R1{weW002ovPDHLkV1jTA?^XZ+ From ef6d87b392e28a32ad48b69f5d94a3716014999f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 30 Oct 2025 20:10:35 +0000 Subject: [PATCH 14/21] Fix max_advance of f32::MAX --- parley/src/layout/line_break.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 6aad4107..d5549105 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -744,7 +744,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { 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.); - let max_advance = if self.state.line_max_advance.is_finite() { + let max_advance = if self.state.line_max_advance < f32::MAX { self.state.line_max_advance } else { line.metrics.advance - line.metrics.trailing_whitespace @@ -770,7 +770,7 @@ impl Drop for BreakLines<'_, B> { // 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::INFINITY { + 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; From e55c5cb608c37017ec1169ba40c0655f6c5cda5b Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 31 Oct 2025 19:14:35 +0000 Subject: [PATCH 15/21] MaxLineHeight WIP Signed-off-by: Nico Burns --- parley/src/layout/line_break.rs | 32 +++++++++++++++++++++++++++++++- parley/src/layout/mod.rs | 4 +++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index d5549105..490da383 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -56,6 +56,8 @@ struct PrevBoundaryState { 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 `break_on_box` set /// to `true` was encountered InlineBoxBreak(BoxBreakData), @@ -67,6 +69,11 @@ pub struct LineBreakData { 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, @@ -75,7 +82,7 @@ pub struct BoxBreakData { pub advance: f32, } -#[derive(Clone, Default)] +#[derive(Clone)] pub struct BreakerState { /// The number of items that have been processed (used to revert state) items: usize, @@ -99,12 +106,34 @@ pub struct BreakerState { 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 pub fn append_cluster_to_line(&mut self, next_x: f32, clusters_height: f32) { @@ -539,6 +568,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.state.line_y += line_break_data.line_height as f64; continue; } + YieldData::MaxHeightExceeded(_) => continue, YieldData::InlineBoxBreak(_) => continue, } } diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index b710b425..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::{BoxBreakData, BreakLines, BreakerState, LineBreakData, YieldData}; +pub use line_break::{ + BoxBreakData, BreakLines, BreakerState, LineBreakData, MaxHeightBreakData, YieldData, +}; pub use run::{Run, RunMetrics}; pub(crate) use data::{LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData}; From c003ac765ce3da137fa6d37d4c66b16b841bd2ce Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 5 Nov 2025 16:01:04 +0000 Subject: [PATCH 16/21] Fix: only InFlow InlineBox's contribute size to the layout --- parley/src/layout/data.rs | 8 +++++--- parley/src/layout/line.rs | 4 +++- parley/src/layout/line_break.rs | 11 ++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) 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/line.rs b/parley/src/layout/line.rs index bf9a5d52..ea2dc116 100644 --- a/parley/src/layout/line.rs +++ b/parley/src/layout/line.rs @@ -265,7 +265,9 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { 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, diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 490da383..ea397bf4 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -360,8 +360,13 @@ impl<'a, B: Brush> BreakLines<'a, B> { })); } + let (width_contribution, height_contribution) = match inline_box.kind { + InlineBoxKind::InFlow => (inline_box.width, inline_box.height), + InlineBoxKind::OutOfFlow => (0.0, 0.0), + }; + // 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); @@ -373,7 +378,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(); @@ -382,7 +387,7 @@ 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(BreakReason::Emergency); From 121cbd6ecce6996f6725da2f0694232d65d1eb9e Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 6 Nov 2025 13:49:08 +0000 Subject: [PATCH 17/21] Make InlineBoxKind Copy --- parley/src/inline_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parley/src/inline_box.rs b/parley/src/inline_box.rs index a58e0fc1..370af0ab 100644 --- a/parley/src/inline_box.rs +++ b/parley/src/inline_box.rs @@ -20,7 +20,7 @@ pub struct InlineBox { pub height: f32, } -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Copy)] pub enum InlineBoxKind { InFlow, OutOfFlow, From 40208b99f6368f55245ee6fc1f094f5a52f93f96 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 6 Nov 2025 13:49:20 +0000 Subject: [PATCH 18/21] Including InlineBoxKind in PositionedInlineBox --- parley/src/layout/line.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/parley/src/layout/line.rs b/parley/src/layout/line.rs index ea2dc116..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. @@ -181,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. @@ -274,6 +275,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { width: inline_box.width, height: inline_box.height, id: inline_box.id, + kind: inline_box.kind, })); } LineItem::Run(run) => { From ca1b9ed0f791a8a41417f8d4b5fdaf009c29b01f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 6 Nov 2025 14:48:46 +0000 Subject: [PATCH 19/21] Replace break_on_box with InlineBoxKind::CustomOutOfFlow Signed-off-by: Nico Burns --- examples/swash_render/src/main.rs | 4 ---- examples/tiny_skia_render/src/main.rs | 1 - parley/src/inline_box.rs | 17 +++++++++++++++-- parley/src/layout/line_break.rs | 26 +++++++++++++------------- parley/src/lib.rs | 2 +- parley/src/tests/test_basic.rs | 11 ----------- parley/src/tests/test_lines.rs | 2 -- 7 files changed, 29 insertions(+), 34 deletions(-) diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index 1ec045fd..7e9278ae 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -95,7 +95,6 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 0, width: 50.0, height: 50.0, @@ -106,7 +105,6 @@ fn main() { builder.push_inline_box(InlineBox { id: 1, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 50, width: 50.0, height: 30.0, @@ -157,7 +155,6 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 40, width: 50.0, height: 50.0, @@ -165,7 +162,6 @@ fn main() { builder.push_inline_box(InlineBox { id: 1, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 50, width: 50.0, height: 30.0, diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs index 30d95176..1c2bbc0a 100644 --- a/examples/tiny_skia_render/src/main.rs +++ b/examples/tiny_skia_render/src/main.rs @@ -89,7 +89,6 @@ fn main() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 40, width: 50.0, height: 50.0, diff --git a/parley/src/inline_box.rs b/parley/src/inline_box.rs index 370af0ab..e7324bda 100644 --- a/parley/src/inline_box.rs +++ b/parley/src/inline_box.rs @@ -9,8 +9,6 @@ pub struct InlineBox { 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, - /// Whether layout should break on this box - pub break_on_box: bool, /// 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, @@ -20,8 +18,23 @@ pub struct InlineBox { 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/line_break.rs b/parley/src/layout/line_break.rs index ea397bf4..31412d8a 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -56,10 +56,10 @@ struct PrevBoundaryState { 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 + /// 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 `break_on_box` set - /// to `true` was encountered + /// Control flow was yielded because an inline box with kind `InlineBoxKind::CustomOutOfFlow` + /// was encountered. InlineBoxBreak(BoxBreakData), } @@ -350,19 +350,19 @@ impl<'a, B: Brush> BreakLines<'a, B> { LayoutItemKind::InlineBox => { let inline_box = &self.layout.data.inline_boxes[item.index]; - // If the box is marked as "break_on_box", then the assumption is that the caller will handle placement of the box. - if inline_box.break_on_box { - 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, - })); - } - 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 diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 88dec5c1..07e5ad32 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -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, kind: InlineBoxKind::InFlow, break_on_box: false, 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); diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs index f47a8bec..47ce7893 100644 --- a/parley/src/tests/test_basic.rs +++ b/parley/src/tests/test_basic.rs @@ -43,7 +43,6 @@ fn placing_inboxes() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: position, width: 10.0, height: 10.0, @@ -65,7 +64,6 @@ fn only_inboxes_wrap() { builder.push_inline_box(InlineBox { id, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 0, width: 10.0, height: 10.0, @@ -88,7 +86,6 @@ fn full_width_inbox() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 1, width: 10., height: 10.0, @@ -96,7 +93,6 @@ fn full_width_inbox() { builder.push_inline_box(InlineBox { id: 1, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 1, width, height: 10.0, @@ -104,7 +100,6 @@ fn full_width_inbox() { builder.push_inline_box(InlineBox { id: 2, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 2, width, height: 10.0, @@ -124,7 +119,6 @@ fn inbox_separated_by_whitespace() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 0, width: 10., height: 10.0, @@ -133,7 +127,6 @@ fn inbox_separated_by_whitespace() { builder.push_inline_box(InlineBox { id: 1, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 1, width: 10.0, height: 10.0, @@ -142,7 +135,6 @@ fn inbox_separated_by_whitespace() { builder.push_inline_box(InlineBox { id: 2, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 2, width: 10.0, height: 10.0, @@ -151,7 +143,6 @@ fn inbox_separated_by_whitespace() { builder.push_inline_box(InlineBox { id: 3, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 3, width: 10.0, height: 10.0, @@ -396,7 +387,6 @@ fn inbox_content_width() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 3, width: 100.0, height: 10.0, @@ -418,7 +408,6 @@ fn inbox_content_width() { builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 2, width: 10.0, height: 10.0, diff --git a/parley/src/tests/test_lines.rs b/parley/src/tests/test_lines.rs index 20b96512..359acb67 100644 --- a/parley/src/tests/test_lines.rs +++ b/parley/src/tests/test_lines.rs @@ -106,7 +106,6 @@ fn build_layout>>( builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 40, width: 50.0, height: 5.0, @@ -114,7 +113,6 @@ fn build_layout>>( builder.push_inline_box(InlineBox { id: 1, kind: InlineBoxKind::InFlow, - break_on_box: false, index: 51, width: 50.0, height: 3.0, From 42ffbb124eb3dc7bf79bc368aaa46e8977b1fb6b Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 6 Nov 2025 15:17:37 +0000 Subject: [PATCH 20/21] Remove is_infinite check in alignment Signed-off-by: Nico Burns --- parley/src/layout/alignment.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/parley/src/layout/alignment.rs b/parley/src/layout/alignment.rs index 6f5fd4df..039f9078 100644 --- a/parley/src/layout/alignment.rs +++ b/parley/src/layout/alignment.rs @@ -110,11 +110,6 @@ fn align_impl( 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; - // FIXME: width should never be infinite. - if !line_width.is_finite() { - continue; - } - if !options.align_when_overflowing && free_space <= 0.0 { if is_rtl { // In RTL text, right-align on overflow. From 90fdd2e0c11e28023a4844222a36bfd90e2eaefa Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 6 Nov 2025 18:05:10 +0000 Subject: [PATCH 21/21] Implement yielding when height is exceeded Signed-off-by: Nico Burns --- parley/src/layout/line_break.rs | 39 ++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index 31412d8a..2e5f43d1 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -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)] @@ -180,6 +181,7 @@ 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) { @@ -255,6 +257,13 @@ impl<'a, B: Brush> BreakLines<'a, B> { } } + 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 } @@ -370,6 +379,11 @@ impl<'a, B: Brush> BreakLines<'a, B> { // 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 { @@ -420,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 { @@ -432,10 +448,11 @@ 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; @@ -475,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 { @@ -486,7 +505,9 @@ 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? @@ -527,7 +548,9 @@ impl<'a, B: Brush> BreakLines<'a, B> { 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; } @@ -573,7 +596,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.state.line_y += line_break_data.line_height as f64; continue; } - YieldData::MaxHeightExceeded(_) => continue, + YieldData::MaxHeightExceeded(_) => continue, // unreachable because max_height is set to f32::MAX YieldData::InlineBoxBreak(_) => continue, } }