Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9ea68d4
Floats WIP
nicoburns Sep 22, 2025
d461cce
Ignore out-of-flow boxes for the purpose of whitespace collapsing
nicoburns Sep 26, 2025
5224c6c
Rename coords to block
nicoburns Sep 27, 2025
55c8a39
Fixup tests (inline box kind)
nicoburns Sep 29, 2025
cf665c9
Allow the caller to set x/y/max_advance for each line
nicoburns Sep 29, 2025
74371a9
Add advance to box yield
nicoburns Sep 30, 2025
06c1966
Align each line to it's own max-width
nicoburns Oct 15, 2025
0a96bf0
Don't include last line in layout's height if it's empty
nicoburns Oct 16, 2025
8b07cb2
Fix Cluster::from_point to work with offset lines
nicoburns Oct 30, 2025
0c097cc
Remove alignment_width argument from tests
nicoburns Oct 30, 2025
1aa1d74
Use inline min coord in more places
nicoburns Oct 30, 2025
2b93c5f
Use layout_max_advance for rendering tests
nicoburns Oct 30, 2025
34e33d2
Accept new snapshots
nicoburns Oct 30, 2025
bf7decd
Fix max_advance of f32::MAX
nicoburns Oct 30, 2025
d4edce2
MaxLineHeight WIP
nicoburns Oct 31, 2025
dc5fda1
Fix: only InFlow InlineBox's contribute size to the layout
nicoburns Nov 5, 2025
cb83592
Make InlineBoxKind Copy
nicoburns Nov 6, 2025
0091e87
Including InlineBoxKind in PositionedInlineBox
nicoburns Nov 6, 2025
9ecda70
Replace break_on_box with InlineBoxKind::CustomOutOfFlow
nicoburns Nov 6, 2025
fbc5d76
Remove is_infinite check in alignment
nicoburns Nov 6, 2025
d8566d3
Implement yielding when height is exceeded
nicoburns Nov 6, 2025
00fdba7
Update content_widths_rtl test snapshot
nicoburns Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions examples/swash_render/src/main.rs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you been able to confirm that these changes do not regress performance (either via parley_bench or in Blitz)?

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,6 +94,7 @@ fn main() {

builder.push_inline_box(InlineBox {
id: 0,
kind: InlineBoxKind::InFlow,
index: 0,
width: 50.0,
height: 50.0,
Expand All @@ -103,6 +104,7 @@ fn main() {

builder.push_inline_box(InlineBox {
id: 1,
kind: InlineBoxKind::InFlow,
index: 50,
width: 50.0,
height: 30.0,
Expand Down Expand Up @@ -152,12 +154,14 @@ fn main() {

builder.push_inline_box(InlineBox {
id: 0,
kind: InlineBoxKind::InFlow,
index: 40,
width: 50.0,
height: 50.0,
});
builder.push_inline_box(InlineBox {
id: 1,
kind: InlineBoxKind::InFlow,
index: 50,
width: 50.0,
height: 30.0,
Expand All @@ -171,7 +175,7 @@ fn main() {

// Perform layout (including bidi resolution and shaping) with start alignment
layout.break_all_lines(max_advance);
layout.align(max_advance, Alignment::Start, AlignmentOptions::default());
layout.align(Alignment::Start, AlignmentOptions::default());

// Create image to render into
let width = layout.width().ceil() as u32 + (padding * 2);
Expand Down
5 changes: 3 additions & 2 deletions examples/tiny_skia_render/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -88,6 +88,7 @@ fn main() {

builder.push_inline_box(InlineBox {
id: 0,
kind: InlineBoxKind::InFlow,
index: 40,
width: 50.0,
height: 50.0,
Expand All @@ -98,7 +99,7 @@ fn main() {

// Perform layout (including bidi resolution and shaping) with start alignment
layout.break_all_lines(max_advance);
layout.align(max_advance, Alignment::Start, AlignmentOptions::default());
layout.align(Alignment::Start, AlignmentOptions::default());
let width = layout.width().ceil() as u32;
let height = layout.height().ceil() as u32;
let padded_width = width + padding * 2;
Expand Down
14 changes: 9 additions & 5 deletions parley/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -112,11 +113,14 @@ impl<B: Brush> 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);
Expand Down
8 changes: 4 additions & 4 deletions parley/src/editing/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,9 @@ fn cursor_rect<B: Brush>(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,
)
}

Expand All @@ -454,9 +454,9 @@ fn last_line_cursor_rect<B: Brush>(layout: &Layout<B>, 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()
Expand Down
2 changes: 1 addition & 1 deletion parley/src/editing/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 5 additions & 5 deletions parley/src/editing/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
23 changes: 23 additions & 0 deletions parley/src/inline_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub struct InlineBox {
/// User-specified identifier for the box, which can be used by the user to determine which box in
/// parley's output corresponds to which box in its input.
pub id: u64,
/// Whether the box is in-flow (takes up space in the layout) or out-of-flow (e.g. absolutely positioned or floated)
pub kind: InlineBoxKind,
/// The byte offset into the underlying text string at which the box should be placed.
/// This must not be within a Unicode code point.
pub index: usize,
Expand All @@ -15,3 +17,24 @@ pub struct InlineBox {
/// The height of the box in pixels
pub height: f32,
}

/// Whether a box is in-flow (takes up space in the layout) or out-of-flow (e.g. absolutely positioned)
/// or custom-out-of-flow (line-breaking should yield control flow)
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum InlineBoxKind {
/// `InFlow` boxes take up space in the layout and flow in line with text
///
/// They correspond to `display: inline-block` boxes in CSS.
InFlow,
/// `OutOfFlow` boxes are assigned a position as if they were a zero-sized inline box, but
/// do not take up space in the layout.
///
/// They correspond to `position: absolute` boxes in CSS.
OutOfFlow,
Comment on lines +25 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add tests for OutOfFlow boxes?

/// `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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the CustomOutOfFlow test could take the form of an exclusion region (like one of the draft tests I mentioned above)

}
4 changes: 2 additions & 2 deletions parley/src/layout/accessibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions parley/src/layout/alignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<B: Brush>(
layout: &mut LayoutData<B>,
alignment_width: Option<f32>,
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);
Expand Down Expand Up @@ -109,8 +107,8 @@ fn align_impl<B: Brush, const UNDO_JUSTIFICATION: bool>(
}

// Compute free space.
let free_space =
layout.alignment_width - line.metrics.advance + line.metrics.trailing_whitespace;
let line_width = line.metrics.inline_max_coord - line.metrics.inline_min_coord;
let free_space = line_width - line.metrics.advance + line.metrics.trailing_whitespace;

if !options.align_when_overflowing && free_space <= 0.0 {
if is_rtl {
Expand Down
5 changes: 2 additions & 3 deletions parley/src/layout/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 10 additions & 8 deletions parley/src/layout/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, TextWrapMode};
use crate::{FontData, InlineBoxKind, LineHeight, OverflowWrap, TextWrapMode};
use core::ops::Range;

use swash::text::cluster::{Boundary, Whitespace};
Expand Down Expand Up @@ -577,13 +577,15 @@ impl<B: Brush> LayoutData<B> {
LayoutItemKind::InlineBox => {
let ibox = &self.inline_boxes[item.index];
running_max_width += ibox.width;
if text_wrap_mode == TextWrapMode::Wrap {
let trailing_whitespace = whitespace_advance(prev_cluster);
min_width = min_width.max(running_min_width - trailing_whitespace);
min_width = min_width.max(ibox.width);
running_min_width = 0.0;
} else {
running_min_width += ibox.width;
if ibox.kind == InlineBoxKind::InFlow {
if text_wrap_mode == TextWrapMode::Wrap {
let trailing_whitespace = whitespace_advance(prev_cluster);
min_width = min_width.max(running_min_width - trailing_whitespace);
min_width = min_width.max(ibox.width);
running_min_width = 0.0;
} else {
running_min_width += ibox.width;
}
}
prev_cluster = None;
}
Expand Down
18 changes: 9 additions & 9 deletions parley/src/layout/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ impl<B: Brush> Layout<B> {
&self.data.styles
}

/// Returns available width of the layout.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please expand the documentation of available_width, width, and full_width so that it's clear how each differ?

pub fn available_width(&self) -> f32 {
self.data.alignment_width
}

/// Returns the width of the layout.
pub fn width(&self) -> f32 {
self.data.width
Expand Down Expand Up @@ -126,14 +131,9 @@ impl<B: Brush> Layout<B> {
/// 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<f32>,
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
Expand Down Expand Up @@ -166,9 +166,9 @@ impl<B: Brush> Layout<B> {
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
Expand Down
Loading
Loading