perf(layout): materialize inline fragments once

This commit is contained in:
ldm0
2026-08-30 00:55:04 +08:00
committed by Donough Liu
parent ebe38d25c7
commit 59fa4f6c8a
2 changed files with 97 additions and 42 deletions
+88 -37
View File
@@ -7,7 +7,12 @@
// Relative positioning of atomic inline boxes additionally follows Blitz
// commit 4a9be930accc971675d5730e4fde3cfa13c3b57e.
use std::{collections::BTreeMap, fmt::Debug, hash::Hash, ops::Range};
use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
hash::Hash,
ops::Range,
};
use parley::{BreakReason, InlineBox, InlineBoxKind, Layout, PositionedLayoutItem, TextStyle};
use taffy::{MaybeResolve as _, Point, Size};
@@ -267,21 +272,6 @@ pub(crate) struct InlineFragments {
pub(crate) boxes: Vec<InlineBoxFragment>,
}
impl InlineFragments {
pub(crate) fn translate_block_axis(&mut self, offset: f32) {
for line in &mut self.lines {
line.rect.y += offset;
line.baseline += offset;
}
for text in &mut self.text {
text.rect.y += offset;
}
for inline_box in &mut self.boxes {
inline_box.rect.y += offset;
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct InlineLineFragment {
pub(crate) line_index: usize,
@@ -314,8 +304,8 @@ pub(crate) fn build_inline_fragments(
line_placements: &[InlineLinePlacement],
) -> InlineFragments {
let mut fragments = InlineFragments::default();
let mut box_fragments = BTreeMap::<(usize, usize), FragmentAccumulator>::new();
let mut source_fragments = BTreeMap::<SourceFragmentKey, FragmentAccumulator>::new();
let mut box_fragments = HashMap::<(usize, usize), FragmentAccumulator>::new();
let mut source_fragments = HashMap::<SourceFragmentKey, FragmentAccumulator>::new();
for (line_index, line) in layout.lines().enumerate() {
let metrics = line.metrics();
@@ -378,11 +368,7 @@ pub(crate) fn build_inline_fragments(
cluster.advance().max(0.0),
(ascent + descent).max(0.0),
);
for unit in context
.text_units
.iter()
.filter(|unit| ranges_overlap(&unit.output_range, &range))
{
for unit in overlapping_output_ranges(&context.text_units, &range) {
for ancestor in &unit.ancestors {
box_fragments
.entry((ancestor.index(), line_index))
@@ -390,11 +376,7 @@ pub(crate) fn build_inline_fragments(
.include_inline_axis(rect.x, rect.width);
}
}
for source in context
.source_map
.iter()
.filter(|source| ranges_overlap(&source.output_range, &range))
{
for source in overlapping_output_ranges(&context.source_map, &range) {
source_fragments
.entry(SourceFragmentKey {
box_index: source.box_id.index(),
@@ -456,6 +438,8 @@ pub(crate) fn build_inline_fragments(
}
}
let mut box_fragments = box_fragments.into_iter().collect::<Vec<_>>();
box_fragments.sort_unstable_by_key(|(key, _)| *key);
fragments.boxes = box_fragments
.into_iter()
.filter_map(|((box_index, line_index), accumulator)| {
@@ -469,6 +453,8 @@ pub(crate) fn build_inline_fragments(
})
})
.collect();
let mut source_fragments = source_fragments.into_iter().collect::<Vec<_>>();
source_fragments.sort_unstable_by_key(|(key, _)| *key);
fragments.text = source_fragments
.into_iter()
.filter_map(|(key, accumulator)| {
@@ -517,9 +503,9 @@ pub(crate) fn break_inline_lines(
let line_range = line.text_range();
metrics.trailing_whitespace > 0.0
&& metrics.advance > width + tolerance
&& context.text_units.iter().any(|unit| {
unit.break_spaces_opportunity && ranges_overlap(&unit.output_range, &line_range)
})
&& overlapping_output_ranges(&context.text_units, &line_range)
.iter()
.any(|unit| unit.break_spaces_opportunity)
})
.collect::<Vec<_>>();
if !adjust_lines.iter().any(|adjust| *adjust) {
@@ -1009,11 +995,9 @@ fn build_line_inline_box_states(
geometries: &[InlineItemVerticalGeometry],
) -> Vec<LineInlineBoxState> {
let mut present = std::collections::BTreeSet::new();
for unit in &context.text_units {
if ranges_overlap(&unit.output_range, &line_range) {
for ancestor in &unit.ancestors {
mark_structural_path(context, *ancestor, &mut present);
}
for unit in overlapping_output_ranges(&context.text_units, &line_range) {
for ancestor in &unit.ancestors {
mark_structural_path(context, *ancestor, &mut present);
}
}
for geometry in geometries {
@@ -1199,7 +1183,7 @@ fn non_edge_vertical_offset(
alignment_shift + baseline_shift
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
struct SourceFragmentKey {
box_index: usize,
source_byte_start: usize,
@@ -1258,6 +1242,43 @@ fn ranges_overlap(left: &Range<usize>, right: &Range<usize>) -> bool {
left.start < right.end && right.start < left.end
}
trait HasInlineOutputRange {
fn output_range(&self) -> &Range<usize>;
}
impl HasInlineOutputRange for InlineTextUnit {
fn output_range(&self) -> &Range<usize> {
&self.output_range
}
}
impl HasInlineOutputRange for InlineSourceMapEntry {
fn output_range(&self) -> &Range<usize> {
&self.output_range
}
}
/// Returns the contiguous slice intersecting `target` from an output-ordered
/// inline map. Normalization produces non-overlapping ranges, except that a
/// single output range can have multiple source origins (for example CRLF
/// merged across text nodes). Visual glyph clusters are not ordered under
/// bidi, so two binary searches are used instead of a stateful cursor.
fn overlapping_output_ranges<'a, T>(items: &'a [T], target: &Range<usize>) -> &'a [T]
where
T: HasInlineOutputRange,
{
if target.is_empty() {
return &items[..0];
}
debug_assert!(items.windows(2).all(|pair| {
pair[0].output_range().start <= pair[1].output_range().start
&& pair[0].output_range().end <= pair[1].output_range().end
}));
let start = items.partition_point(|item| item.output_range().end <= target.start);
let end = start + items[start..].partition_point(|item| item.output_range().start < target.end);
&items[start..end]
}
pub(crate) fn prepare_inline_contexts<N>(
world: &mut LayoutWorld<N>,
services: &mut DocumentLayoutServices,
@@ -2117,6 +2138,36 @@ fn is_combining_mark(character: char) -> bool {
mod tests {
use super::*;
#[test]
fn ordered_output_range_lookup_handles_duplicates_gaps_and_bidi_order() {
let source = |output_range: Range<usize>| InlineSourceMapEntry {
output_range,
box_id: LayoutBoxId::from_index(1),
source_byte_range: 0..1,
source_utf16_range: 0..1,
};
let entries = vec![
source(0..1),
source(1..2),
source(1..2),
source(2..5),
source(7..9),
];
let ranges = |query: Range<usize>| {
overlapping_output_ranges(&entries, &query)
.iter()
.map(|entry| entry.output_range.clone())
.collect::<Vec<_>>()
};
assert_eq!(ranges(1..3), vec![1..2, 1..2, 2..5]);
// Visual clusters may query logical text in either direction.
assert_eq!(ranges(7..8), vec![7..9]);
assert_eq!(ranges(0..1), vec![0..1]);
assert!(ranges(5..7).is_empty());
assert!(ranges(2..2).is_empty());
}
#[test]
fn normal_line_height_unites_metrics_from_the_shaped_fallback_font() {
let primary = InlineStrutMetrics {
+9 -5
View File
@@ -1775,6 +1775,14 @@ where
output.margins_can_collapse_through = false;
}
if inputs.run_mode == RunMode::PerformLayout {
// Intrinsic and flex/grid probes need only the numeric IFC
// result. Materialize CSSOM/paint fragments once, after the
// accepted layout has received its final block alignment.
let fragments = build_inline_fragments(
&inline_context,
&measurement.layout,
&measurement.line_placements,
);
self.position_inline_objects(
&inline_context,
&measurement,
@@ -1785,7 +1793,7 @@ where
content_box_size,
self.boxes[id.index()].style.direction(),
);
inline_context.fragments = measurement.fragments;
inline_context.fragments = fragments;
inline_context.line_placements = measurement.line_placements;
inline_context.laid_out = Some(measurement.layout);
}
@@ -2133,7 +2141,6 @@ where
.rev()
.find(|line| !line.phantom)
.map(|line| line.baseline);
let fragments = build_inline_fragments(context, &layout, &line_placements);
InlineMeasurement {
size: Size {
width: known_dimensions.width.unwrap_or(width),
@@ -2147,7 +2154,6 @@ where
floats,
percentage_basis,
line_placements,
fragments,
}
}
@@ -2533,7 +2539,6 @@ struct InlineMeasurement {
floats: Vec<InlineFloatPlacement>,
percentage_basis: Option<f32>,
line_placements: Vec<InlineLinePlacement>,
fragments: InlineFragments,
}
impl InlineMeasurement {
@@ -2553,7 +2558,6 @@ impl InlineMeasurement {
for floated in &mut self.floats {
floated.location.y += offset;
}
self.fragments.translate_block_axis(offset);
}
}