Skip to content

Commit 43ba11d

Browse files
authored
Calculate minimum and maximum content width (#259)
This adds `min_content_width` and `max_content_width` functions to the layout. These functions provide an lower and upper bound on the layout width. The content widths are trailing white space aware, which means that they correspond to `layout.width` and not `layout.full_width`. Follow-up work includes using the `max_content_width` to ensure the `max_advance` is always capped. This will become useful for floated boxes (#99), because those require Parley to align individual lines based on their `max_advance`, which in turn requires a reasonable upper bound on `max_advance`.
1 parent 2d177d2 commit 43ba11d

10 files changed

+209
-1
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This release has an [MSRV] of 1.82.
2020
#### Fontique
2121

2222
- `FontStretch`, `FontStyle`, and `FontWeight` get helper functions `from_fontconfig` ([#212] by [@waywardmonkeys][])
23+
- `Layout` methods to calculate minimum and maximum content widths. ([#259][] by [@wfdewith][])
2324

2425
#### Parley
2526

@@ -38,6 +39,7 @@ This release has an [MSRV] of 1.82.
3839
- Breaking change: `PlainEditor`'s semantics are no longer transactional ([#192][] by [@DJMcNab][])
3940
- Breaking change: `Alignment::Start` and `Alignment::End` now depend on text base direction.
4041
`Alignment::Left` and `Alignment::Right` are introduced for text direction-independent alignment. ([#250][] by [@tomcur][])
42+
- Breaking change: `Layout` is no longer `Sync`. ([#259][] by [@wfdewith][])
4143

4244
### Fixed
4345

@@ -103,6 +105,7 @@ This release has an [MSRV] of 1.70.
103105
[@nicoburns]: https://github.com/nicoburns
104106
[@tomcur]: https://github.com/tomcur
105107
[@waywardmonkeys]: https://github.com/waywardmonkeys
108+
[@wfdewith]: https://github.com/wfdewith
106109
[@xorgy]: https://github.com/xorgy
107110

108111
[#54]: https://github.com/linebender/parley/pull/54
@@ -124,6 +127,7 @@ This release has an [MSRV] of 1.70.
124127
[#223]: https://github.com/linebender/parley/pull/223
125128
[#224]: https://github.com/linebender/parley/pull/224
126129
[#250]: https://github.com/linebender/parley/pull/250
130+
[#259]: https://github.com/linebender/parley/pull/259
127131
[#268]: https://github.com/linebender/parley/pull/268
128132

129133
[Unreleased]: https://github.com/linebender/parley/compare/v0.2.0...HEAD

parley/src/layout/data.rs

+76-1
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

44
use crate::inline_box::InlineBox;
5-
use crate::layout::{Alignment, Glyph, LineMetrics, RunMetrics, Style};
5+
use crate::layout::{Alignment, ContentWidths, Glyph, LineMetrics, RunMetrics, Style};
66
use crate::style::Brush;
77
use crate::util::nearly_zero;
88
use crate::Font;
9+
use core::cell::OnceCell;
910
use core::ops::Range;
1011
use swash::shape::Shaper;
1112
use swash::text::cluster::{Boundary, ClusterInfo};
1213
use swash::Synthesis;
1314

1415
use alloc::vec::Vec;
1516

17+
#[cfg(feature = "libm")]
18+
#[allow(unused_imports)]
19+
use core_maths::CoreFloat;
20+
1621
#[derive(Copy, Clone)]
1722
pub(crate) struct ClusterData {
1823
pub(crate) info: ClusterInfo,
@@ -199,6 +204,9 @@ pub(crate) struct LayoutData<B: Brush> {
199204
pub(crate) fonts: Vec<Font>,
200205
pub(crate) coords: Vec<i16>,
201206

207+
// Lazily calculated values
208+
content_widths: OnceCell<ContentWidths>,
209+
202210
// Input (/ output of style resolution)
203211
pub(crate) styles: Vec<Style<B>>,
204212
pub(crate) inline_boxes: Vec<InlineBox>,
@@ -223,6 +231,7 @@ impl<B: Brush> Default for LayoutData<B> {
223231
text_len: 0,
224232
width: 0.,
225233
full_width: 0.,
234+
content_widths: OnceCell::new(),
226235
height: 0.,
227236
fonts: Vec::new(),
228237
coords: Vec::new(),
@@ -246,6 +255,7 @@ impl<B: Brush> LayoutData<B> {
246255
self.text_len = 0;
247256
self.width = 0.;
248257
self.full_width = 0.;
258+
self.content_widths.take();
249259
self.height = 0.;
250260
self.fonts.clear();
251261
self.coords.clear();
@@ -470,4 +480,69 @@ impl<B: Brush> LayoutData<B> {
470480
}
471481
}
472482
}
483+
484+
pub(crate) fn content_widths(&self) -> ContentWidths {
485+
*self
486+
.content_widths
487+
.get_or_init(|| self.calculate_content_widths())
488+
}
489+
490+
// TODO: this method does not handle mixed direction text at all.
491+
fn calculate_content_widths(&self) -> ContentWidths {
492+
fn whitespace_advance(cluster: Option<&ClusterData>) -> f32 {
493+
cluster
494+
.filter(|cluster| cluster.info.whitespace().is_space_or_nbsp())
495+
.map_or(0.0, |cluster| cluster.advance)
496+
}
497+
498+
let mut min_width = 0.0_f32;
499+
let mut max_width = 0.0_f32;
500+
501+
let mut running_max_width = 0.0;
502+
let mut prev_cluster: Option<&ClusterData> = None;
503+
let is_rtl = self.base_level & 1 == 1;
504+
for item in &self.items {
505+
match item.kind {
506+
LayoutItemKind::TextRun => {
507+
let run = &self.runs[item.index];
508+
let mut running_min_width = 0.0;
509+
let clusters = &self.clusters[run.cluster_range.clone()];
510+
if is_rtl {
511+
prev_cluster = clusters.first();
512+
}
513+
for cluster in clusters {
514+
let boundary = cluster.info.boundary();
515+
if matches!(boundary, Boundary::Line | Boundary::Mandatory) {
516+
let trailing_whitespace = whitespace_advance(prev_cluster);
517+
min_width = min_width.max(running_min_width - trailing_whitespace);
518+
running_min_width = 0.0;
519+
if boundary == Boundary::Mandatory {
520+
running_max_width = 0.0;
521+
}
522+
}
523+
running_min_width += cluster.advance;
524+
running_max_width += cluster.advance;
525+
if !is_rtl {
526+
prev_cluster = Some(cluster);
527+
}
528+
}
529+
let trailing_whitespace = whitespace_advance(prev_cluster);
530+
min_width = min_width.max(running_min_width - trailing_whitespace);
531+
}
532+
LayoutItemKind::InlineBox => {
533+
let ibox = &self.inline_boxes[item.index];
534+
min_width = min_width.max(ibox.width);
535+
running_max_width += ibox.width;
536+
prev_cluster = None;
537+
}
538+
}
539+
let trailing_whitespace = whitespace_advance(prev_cluster);
540+
max_width = max_width.max(running_max_width - trailing_whitespace);
541+
}
542+
543+
ContentWidths {
544+
min: min_width,
545+
max: max_width,
546+
}
547+
}
473548
}

parley/src/layout/mod.rs

+28
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,23 @@ impl<B: Brush> Layout<B> {
9595
self.data.full_width
9696
}
9797

98+
/// Returns the lower and upper bounds on the width of the layout.
99+
pub fn content_widths(&self) -> ContentWidths {
100+
self.data.content_widths()
101+
}
102+
103+
/// Returns the minimum content width of the layout. This is the width of the layout if _all_
104+
/// soft line-breaking opportunities are taken.
105+
pub fn min_content_width(&self) -> f32 {
106+
self.data.content_widths().min
107+
}
108+
109+
/// Returns the maximum content width of the layout. This is the width of the layout if _no_
110+
/// soft line-breaking opportunities are taken.
111+
pub fn max_content_width(&self) -> f32 {
112+
self.data.content_widths().max
113+
}
114+
98115
/// Returns the height of the layout.
99116
pub fn height(&self) -> f32 {
100117
self.data.height
@@ -451,3 +468,14 @@ impl LayoutAccessibility {
451468
}
452469
}
453470
}
471+
472+
/// Lower and upper bounds on layout width based on its contents.
473+
#[derive(Copy, Clone, Debug)]
474+
pub struct ContentWidths {
475+
/// The minimum content width. This is the width of the layout if _all_ soft line-breaking
476+
/// opportunities are taken.
477+
pub min: f32,
478+
/// The maximum content width. This is the width of the layout if _no_ soft line-breaking
479+
/// opportunities are taken.
480+
pub max: f32,
481+
}

parley/src/tests/test_basic.rs

+83
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,86 @@ fn overflow_alignment_rtl() {
218218
env.rendering_config().size = Some(Size::new(10., layout.height().into()));
219219
env.check_layout_snapshot(&layout);
220220
}
221+
222+
#[test]
223+
fn content_widths() {
224+
let mut env = testenv!();
225+
226+
let text = "Hello world!\nLonger line with a looooooooong word.";
227+
let mut builder = env.ranged_builder(text);
228+
229+
let mut layout = builder.build(text);
230+
231+
layout.break_all_lines(Some(layout.min_content_width()));
232+
layout.align(None, Alignment::Start, false);
233+
env.with_name("min").check_layout_snapshot(&layout);
234+
235+
layout.break_all_lines(Some(layout.max_content_width()));
236+
layout.align(None, Alignment::Start, false);
237+
env.with_name("max").check_layout_snapshot(&layout);
238+
}
239+
240+
#[test]
241+
fn content_widths_rtl() {
242+
let mut env = testenv!();
243+
244+
let text = "بببب ااااا";
245+
let mut builder = env.ranged_builder(text);
246+
247+
let mut layout = builder.build(text);
248+
249+
layout.break_all_lines(Some(layout.min_content_width()));
250+
layout.align(None, Alignment::Start, false);
251+
env.with_name("min").check_layout_snapshot(&layout);
252+
253+
layout.break_all_lines(Some(layout.max_content_width()));
254+
layout.align(None, Alignment::Start, false);
255+
assert!(
256+
layout.width() <= layout.max_content_width(),
257+
"Layout should never be wider than the max content width"
258+
);
259+
env.with_name("max").check_layout_snapshot(&layout);
260+
}
261+
262+
#[test]
263+
fn inbox_content_width() {
264+
let mut env = testenv!();
265+
266+
{
267+
let text = "Hello world!";
268+
let mut builder = env.ranged_builder(text);
269+
builder.push_inline_box(InlineBox {
270+
id: 0,
271+
index: 3,
272+
width: 100.0,
273+
height: 10.0,
274+
});
275+
let mut layout = builder.build(text);
276+
layout.break_all_lines(Some(layout.min_content_width()));
277+
layout.align(None, Alignment::Start, false);
278+
279+
env.with_name("full_width").check_layout_snapshot(&layout);
280+
}
281+
282+
{
283+
let text = "A ";
284+
let mut builder = env.ranged_builder(text);
285+
builder.push_inline_box(InlineBox {
286+
id: 0,
287+
index: 2,
288+
width: 10.0,
289+
height: 10.0,
290+
});
291+
let mut layout = builder.build(text);
292+
layout.break_all_lines(Some(layout.max_content_width()));
293+
layout.align(None, Alignment::Start, false);
294+
295+
assert!(
296+
layout.width() <= layout.max_content_width(),
297+
"Layout should never be wider than the max content width"
298+
);
299+
300+
env.with_name("trailing_whitespace")
301+
.check_layout_snapshot(&layout);
302+
}
303+
}
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)