Skip to content

Commit 9751217

Browse files
authored
Fix and simplify unjustify, remove LineData::alignment (#271)
`unjustify` doesn't mirror `align`, so it performs the wrong calculations, and re-line-breaking or re-aligning currently lead to the wrong result after a justified alignment. As the alignment function requires further iteration (e.g. for floated boxes), this patch removes the specialized `unjustify` implementation and instead uses the same code for aligning and unjustifying. This is a trade-off between efficiency and correctness. If this is merged in, we can always split the two again in the future once alignment has stabilized. This stores the width the layout was aligned to, which is necessary for unjustifying.
1 parent f435705 commit 9751217

File tree

7 files changed

+84
-68
lines changed

7 files changed

+84
-68
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ This release has an [MSRV] of 1.82.
4646
#### Parley
4747

4848
- Fix alignment of right-to-left text. ([#250][], [#268][] by [@tomcur][])
49+
- Performing line breaking or aligning a layout again, after justified alignment had been applied previously, now lead to the correct results. ([#271][] by [@tomcur][])
4950

5051
## [0.2.0] - 2024-10-10
5152

@@ -129,6 +130,7 @@ This release has an [MSRV] of 1.70.
129130
[#250]: https://github.com/linebender/parley/pull/250
130131
[#259]: https://github.com/linebender/parley/pull/259
131132
[#268]: https://github.com/linebender/parley/pull/268
133+
[#271]: https://github.com/linebender/parley/pull/271
132134

133135
[Unreleased]: https://github.com/linebender/parley/compare/v0.2.0...HEAD
134136
[0.2.0]: https://github.com/linebender/parley/releases/tag/v0.2.0

parley/src/layout/alignment.rs

+39-58
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,61 @@ use super::{
77
};
88
use crate::style::Brush;
99

10+
/// Align the layout.
11+
///
12+
/// If [`Alignment::Justified`] is requested, clusters' [`ClusterData::advance`] will be adjusted.
13+
/// Prior to re-line-breaking or re-aligning, [`unjustify`] has to be called.
1014
pub(crate) fn align<B: Brush>(
1115
layout: &mut LayoutData<B>,
1216
alignment_width: Option<f32>,
1317
alignment: Alignment,
1418
align_when_overflowing: bool,
19+
) {
20+
layout.alignment_width = alignment_width.unwrap_or(layout.width);
21+
layout.is_aligned_justified = alignment == Alignment::Justified;
22+
23+
align_impl::<_, false>(layout, alignment, align_when_overflowing);
24+
}
25+
26+
/// Removes previous justification applied to clusters.
27+
///
28+
/// This is part of resetting state in preparation for re-line-breaking or re-aligning the same
29+
/// layout.
30+
pub(crate) fn unjustify<B: Brush>(layout: &mut LayoutData<B>) {
31+
if layout.is_aligned_justified {
32+
align_impl::<_, true>(layout, Alignment::Justified, false);
33+
layout.is_aligned_justified = false;
34+
}
35+
}
36+
37+
/// The actual alignment implementation.
38+
///
39+
/// This is const-generic over `UNDO_JUSTIFICATION`: justified alignment adjusts clusters'
40+
/// [`ClusterData::advance`], and this mutation has to be undone for re-line-breaking or
41+
/// re-aligning. `UNDO_JUSTIFICATION` indicates whether the adjustment has to be applied, or
42+
/// undone.
43+
///
44+
/// Writing a separate function for undoing justification would be faster, but not by much, and
45+
/// doing it this way we are sure the calculations performed are equivalent.
46+
fn align_impl<B: Brush, const UNDO_JUSTIFICATION: bool>(
47+
layout: &mut LayoutData<B>,
48+
alignment: Alignment,
49+
align_when_overflowing: bool,
1550
) {
1651
// Whether the text base direction is right-to-left.
1752
let is_rtl = layout.base_level & 1 == 1;
18-
let alignment_width = alignment_width.unwrap_or(layout.width);
1953

2054
// Apply alignment to line items
2155
for line in &mut layout.lines {
22-
// TODO: remove this field
23-
line.alignment = alignment;
24-
2556
if is_rtl {
2657
// In RTL text, trailing whitespace is on the left. As we hang that whitespace, offset
2758
// the line to the left.
2859
line.metrics.offset = -line.metrics.trailing_whitespace;
2960
}
3061

3162
// Compute free space.
32-
let free_space = alignment_width - line.metrics.advance + line.metrics.trailing_whitespace;
63+
let free_space =
64+
layout.alignment_width - line.metrics.advance + line.metrics.trailing_whitespace;
3365

3466
if !align_when_overflowing && free_space <= 0.0 {
3567
if is_rtl {
@@ -65,7 +97,8 @@ pub(crate) fn align<B: Brush>(
6597
continue;
6698
}
6799

68-
let adjustment = free_space / line.num_spaces as f32;
100+
let adjustment =
101+
free_space / line.num_spaces as f32 * if UNDO_JUSTIFICATION { -1. } else { 1. };
69102
let mut applied = 0;
70103
// Iterate over text runs in the line and clusters in the text run
71104
// - Iterate forwards for even bidi levels (which represent LTR runs)
@@ -100,55 +133,3 @@ pub(crate) fn align<B: Brush>(
100133
}
101134
}
102135
}
103-
104-
/// Removes previous justification applied to clusters.
105-
/// This is part of resetting state in preparation for re-linebreaking the same layout.
106-
pub(crate) fn unjustify<B: Brush>(layout: &mut LayoutData<B>) {
107-
// Whether the text base direction is right-to-left.
108-
let is_rtl = layout.base_level & 1 == 1;
109-
110-
for line in &layout.lines {
111-
if line.alignment == Alignment::Justified
112-
&& line.max_advance.is_finite()
113-
&& line.max_advance < f32::MAX
114-
{
115-
let extra = line.max_advance - line.metrics.advance + line.metrics.trailing_whitespace;
116-
if line.break_reason != BreakReason::None && line.num_spaces != 0 {
117-
let adjustment = extra / line.num_spaces as f32;
118-
let mut applied = 0;
119-
120-
let line_items: &mut dyn Iterator<Item = &LineItemData> = if is_rtl {
121-
&mut layout.line_items[line.item_range.clone()].iter().rev()
122-
} else {
123-
&mut layout.line_items[line.item_range.clone()].iter()
124-
};
125-
for line_run in line_items.filter(|item| item.is_text_run()) {
126-
if line_run.bidi_level & 1 != 0 {
127-
for cluster in layout.clusters[line_run.cluster_range.clone()]
128-
.iter_mut()
129-
.rev()
130-
{
131-
if applied == line.num_spaces {
132-
break;
133-
}
134-
if cluster.info.whitespace().is_space_or_nbsp() {
135-
cluster.advance -= adjustment;
136-
applied += 1;
137-
}
138-
}
139-
} else {
140-
for cluster in layout.clusters[line_run.cluster_range.clone()].iter_mut() {
141-
if applied == line.num_spaces {
142-
break;
143-
}
144-
if cluster.info.whitespace().is_space_or_nbsp() {
145-
cluster.advance -= adjustment;
146-
applied += 1;
147-
}
148-
}
149-
}
150-
}
151-
}
152-
}
153-
}
154-
}

parley/src/layout/data.rs

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

44
use crate::inline_box::InlineBox;
5-
use crate::layout::{Alignment, ContentWidths, Glyph, LineMetrics, RunMetrics, Style};
5+
use crate::layout::{ContentWidths, Glyph, LineMetrics, RunMetrics, Style};
66
use crate::style::Brush;
77
use crate::util::nearly_zero;
88
use crate::Font;
@@ -105,8 +105,10 @@ pub(crate) struct LineData {
105105
pub(crate) metrics: LineMetrics,
106106
/// The cause of the line break.
107107
pub(crate) break_reason: BreakReason,
108-
/// Alignment.
109-
pub(crate) alignment: Alignment,
108+
#[expect(
109+
dead_code,
110+
reason = "needed in the future for aligning around floated boxes"
111+
)]
110112
/// Maximum advance for the line.
111113
pub(crate) max_advance: f32,
112114
/// Number of justified clusters on the line.
@@ -220,6 +222,12 @@ pub(crate) struct LayoutData<B: Brush> {
220222
// Output of line breaking
221223
pub(crate) lines: Vec<LineData>,
222224
pub(crate) line_items: Vec<LineItemData>,
225+
226+
// Output of alignment
227+
/// Whether the layout is aligned with [`crate::Alignment::Justified`].
228+
pub(crate) is_aligned_justified: bool,
229+
/// The width the layout was aligned to.
230+
pub(crate) alignment_width: f32,
223231
}
224232

225233
impl<B: Brush> Default for LayoutData<B> {
@@ -243,6 +251,8 @@ impl<B: Brush> Default for LayoutData<B> {
243251
glyphs: Vec::new(),
244252
lines: Vec::new(),
245253
line_items: Vec::new(),
254+
is_aligned_justified: false,
255+
alignment_width: 0.0,
246256
}
247257
}
248258
}

parley/src/layout/line/greedy.rs

+2-7
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ use swash::text::cluster::Whitespace;
1010
#[allow(unused_imports)]
1111
use core_maths::CoreFloat;
1212

13-
use crate::layout::alignment::unjustify;
1413
use crate::layout::{
15-
Alignment, Boundary, BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData,
16-
LineItemData, LineMetrics, Run,
14+
Boundary, BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData,
15+
LineMetrics, Run,
1716
};
1817
use crate::style::Brush;
1918

@@ -108,7 +107,6 @@ pub struct BreakLines<'a, B: Brush> {
108107

109108
impl<'a, B: Brush> BreakLines<'a, B> {
110109
pub(crate) fn new(layout: &'a mut Layout<B>) -> Self {
111-
unjustify(&mut layout.data);
112110
layout.data.width = 0.;
113111
layout.data.height = 0.;
114112
let mut lines = LineLayout::default();
@@ -166,7 +164,6 @@ impl<'a, B: Brush> BreakLines<'a, B> {
166164
&mut self.lines,
167165
&mut self.state.line,
168166
max_advance,
169-
Alignment::Start,
170167
$break_reason,
171168
)
172169
};
@@ -699,7 +696,6 @@ fn try_commit_line<B: Brush>(
699696
lines: &mut LineLayout,
700697
state: &mut LineState,
701698
max_advance: f32,
702-
alignment: Alignment,
703699
break_reason: BreakReason,
704700
) -> bool {
705701
// Ensure that the cluster and item endpoints are within range
@@ -820,7 +816,6 @@ fn try_commit_line<B: Brush>(
820816
lines.lines.push(LineData {
821817
item_range: start_item_idx..end_item_idx,
822818
max_advance,
823-
alignment,
824819
break_reason,
825820
num_spaces,
826821
metrics: LineMetrics {

parley/src/layout/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use super::style::Brush;
1919
use crate::{Font, InlineBox};
2020
#[cfg(feature = "accesskit")]
2121
use accesskit::{Node, NodeId, Role, TextDirection, TreeUpdate};
22+
use alignment::unjustify;
2223
#[cfg(feature = "accesskit")]
2324
use alloc::vec::Vec;
2425
use core::{cmp::Ordering, ops::Range};
@@ -164,6 +165,7 @@ impl<B: Brush> Layout<B> {
164165

165166
/// Returns line breaker to compute lines for the layout.
166167
pub fn break_lines(&mut self) -> BreakLines<'_, B> {
168+
unjustify(&mut self.data);
167169
BreakLines::new(self)
168170
}
169171

@@ -189,6 +191,7 @@ impl<B: Brush> Layout<B> {
189191
alignment: Alignment,
190192
align_when_overflowing: bool,
191193
) {
194+
unjustify(&mut self.data);
192195
align(
193196
&mut self.data,
194197
container_width,

parley/src/tests/test_basic.rs

+22
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,25 @@ fn inbox_content_width() {
301301
.check_layout_snapshot(&layout);
302302
}
303303
}
304+
305+
#[test]
306+
/// Layouts can be re-line-breaked and re-aligned.
307+
fn realign() {
308+
let mut env = testenv!();
309+
310+
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
311+
let mut builder = env.ranged_builder(text);
312+
let mut layout = builder.build(text);
313+
layout.break_all_lines(Some(150.0));
314+
for idx in 0..8 {
315+
if [2, 3, 4].contains(&idx) {
316+
layout.break_all_lines(Some(150.0));
317+
}
318+
layout.align(
319+
Some(150.),
320+
Alignment::Justified,
321+
false, /* align_when_overflowing */
322+
);
323+
}
324+
env.check_layout_snapshot(&layout);
325+
}

parley/tests/snapshots/realign-0.png

+3
Loading

0 commit comments

Comments
 (0)