Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
729 lines
22 KiB
Rust
729 lines
22 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
// cSpell: ignore ellow orld Hellow
|
|
|
|
use core::ops::Range;
|
|
|
|
use euclid::num::Zero;
|
|
|
|
use crate::items::TextWrap;
|
|
|
|
use super::fragments::{TextFragment, TextFragmentIterator};
|
|
use super::{CheckedAdd, ShapeBuffer, TextShaper};
|
|
|
|
#[derive(Clone, Default, Debug)]
|
|
pub struct TextLine<Length: Default + Clone> {
|
|
// The range excludes trailing whitespace
|
|
pub byte_range: Range<usize>,
|
|
// number of bytes in text after byte_range occupied by trailing whitespace
|
|
pub trailing_whitespace_bytes: usize,
|
|
pub(crate) glyph_range: Range<usize>,
|
|
trailing_whitespace: Length,
|
|
pub(crate) text_width: Length, // with as occupied by the glyphs
|
|
}
|
|
|
|
impl<Length: Default + Copy + Clone + Zero + CheckedAdd + core::cmp::PartialOrd> TextLine<Length> {
|
|
pub fn line_text<'a>(&self, paragraph: &'a str) -> &'a str {
|
|
¶graph[self.byte_range.clone()]
|
|
}
|
|
|
|
pub fn width_including_trailing_whitespace(&self) -> Length {
|
|
if self.text_width > Length::zero() {
|
|
self.text_width.saturating_add(self.trailing_whitespace)
|
|
} else {
|
|
Length::zero()
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.byte_range.is_empty()
|
|
}
|
|
}
|
|
|
|
impl<Length: Clone + Copy + Default + CheckedAdd + core::ops::AddAssign> TextLine<Length> {
|
|
pub fn add_fragment(&mut self, fragment: &TextFragment<Length>) {
|
|
if self.byte_range.is_empty() {
|
|
self.byte_range = fragment.byte_range.clone();
|
|
} else if !fragment.byte_range.is_empty() {
|
|
self.byte_range.end = fragment.byte_range.end;
|
|
}
|
|
if self.glyph_range.is_empty() {
|
|
self.glyph_range = fragment.glyph_range.clone();
|
|
} else {
|
|
self.glyph_range.end = fragment.glyph_range.end;
|
|
}
|
|
if !fragment.byte_range.is_empty() {
|
|
self.text_width = self.text_width.saturating_add(self.trailing_whitespace);
|
|
self.trailing_whitespace = Length::default();
|
|
self.trailing_whitespace_bytes = 0;
|
|
}
|
|
self.text_width = self.text_width.saturating_add(fragment.width);
|
|
self.trailing_whitespace =
|
|
self.trailing_whitespace.saturating_add(fragment.trailing_whitespace_width);
|
|
self.trailing_whitespace_bytes += fragment.trailing_whitespace_bytes;
|
|
}
|
|
}
|
|
|
|
pub struct TextLineBreaker<'a, Font: TextShaper> {
|
|
fragments: TextFragmentIterator<'a, Font::Length>,
|
|
available_width: Option<Font::Length>,
|
|
current_line: TextLine<Font::Length>,
|
|
num_emitted_lines: usize,
|
|
mandatory_line_break_on_next_iteration: bool,
|
|
max_lines: Option<usize>,
|
|
text_wrap: TextWrap,
|
|
done: bool,
|
|
}
|
|
|
|
impl<'a, Font: TextShaper> TextLineBreaker<'a, Font> {
|
|
pub fn new(
|
|
text: &'a str,
|
|
shape_buffer: &'a ShapeBuffer<Font::Length>,
|
|
available_width: Option<Font::Length>,
|
|
max_lines: Option<usize>,
|
|
text_wrap: TextWrap,
|
|
) -> Self {
|
|
Self {
|
|
fragments: TextFragmentIterator::new(text, shape_buffer),
|
|
available_width,
|
|
current_line: Default::default(),
|
|
num_emitted_lines: 0,
|
|
mandatory_line_break_on_next_iteration: false,
|
|
max_lines,
|
|
text_wrap,
|
|
done: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Font: TextShaper> Iterator for TextLineBreaker<'_, Font> {
|
|
type Item = TextLine<Font::Length>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if self.done {
|
|
return None;
|
|
}
|
|
|
|
if let Some(max_lines) = self.max_lines
|
|
&& self.num_emitted_lines >= max_lines
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if core::mem::take(&mut self.mandatory_line_break_on_next_iteration) {
|
|
self.num_emitted_lines += 1;
|
|
return Some(core::mem::take(&mut self.current_line));
|
|
}
|
|
|
|
self.fragments.break_anywhere = false;
|
|
|
|
let mut next_line = loop {
|
|
// Clone the fragment iterator so that we can roll back in case we must break down the first
|
|
// word with `break_anywhere = true`.
|
|
let mut fragments = self.fragments.clone();
|
|
|
|
let fragment = match fragments.next() {
|
|
Some(fragment) => fragment,
|
|
None => {
|
|
break None;
|
|
}
|
|
};
|
|
|
|
// As trailing_mandatory_break is only set if break_anywhere is false, the fragment must
|
|
// be first processed with break_anywhere = false and if no mandatory break is found, the
|
|
// loop is re-run with break_anywhere = true when using CharWrap.
|
|
if self.text_wrap == TextWrap::CharWrap
|
|
&& !fragment.trailing_mandatory_break
|
|
&& !self.fragments.break_anywhere
|
|
{
|
|
self.fragments.break_anywhere = true;
|
|
continue;
|
|
}
|
|
|
|
if let Some(available_width) = self.available_width
|
|
&& self.current_line.width_including_trailing_whitespace() + fragment.width
|
|
> available_width
|
|
{
|
|
if self.current_line.is_empty() {
|
|
if !self.fragments.break_anywhere {
|
|
// Try again but break anywhere this time. self.fragments is cloned at the beginning
|
|
// of the loop.
|
|
self.fragments.break_anywhere = true;
|
|
continue;
|
|
} else {
|
|
// Even if we allow to break anywhere, there is still no room for the next fragment.
|
|
// Just use it anywhere otherwise we would return many empty lines
|
|
self.fragments = fragments;
|
|
self.current_line.add_fragment(&fragment);
|
|
break Some(core::mem::take(&mut self.current_line));
|
|
}
|
|
}
|
|
|
|
let next_line = core::mem::take(&mut self.current_line);
|
|
self.mandatory_line_break_on_next_iteration = fragment.trailing_mandatory_break;
|
|
|
|
if self.text_wrap != TextWrap::CharWrap
|
|
&& !fragments.break_anywhere
|
|
&& fragment.width < available_width
|
|
{
|
|
self.current_line.add_fragment(&fragment);
|
|
self.fragments = fragments;
|
|
}
|
|
|
|
break Some(next_line);
|
|
};
|
|
|
|
self.fragments = fragments;
|
|
self.current_line.add_fragment(&fragment);
|
|
|
|
// The fragment reached the width the coordinate type can represent; the rest of the
|
|
// line is off any viewport, so emit what we have and stop.
|
|
if self.fragments.truncated {
|
|
self.done = true;
|
|
break Some(core::mem::take(&mut self.current_line));
|
|
}
|
|
|
|
if fragment.trailing_mandatory_break {
|
|
break Some(core::mem::take(&mut self.current_line));
|
|
}
|
|
};
|
|
|
|
// Emit at least one single line
|
|
if next_line.is_none()
|
|
&& (!self.current_line.byte_range.is_empty() || self.num_emitted_lines == 0)
|
|
{
|
|
next_line = Some(core::mem::take(&mut self.current_line));
|
|
}
|
|
|
|
if next_line.is_some() {
|
|
self.num_emitted_lines += 1;
|
|
}
|
|
|
|
next_line
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
use super::{FixedTestFont, TextLayout};
|
|
|
|
#[test]
|
|
fn test_empty_line_break() {
|
|
let font = FixedTestFont;
|
|
let text = "";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 1);
|
|
assert_eq!(lines[0].line_text(text), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_char_wrap() {
|
|
// The available width is half-way into the next word
|
|
let font = FixedTestFont;
|
|
let text = "Hello World";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(80.),
|
|
None,
|
|
TextWrap::CharWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "Hello Wo");
|
|
assert_eq!(lines[1].line_text(text), "rld");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello World";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_max_lines() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello World";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
Some(1),
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 1);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
}
|
|
|
|
#[test]
|
|
fn test_linebreak_trailing_space() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello ";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 1);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
}
|
|
|
|
#[test]
|
|
fn test_forced_break() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello\nWorld";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines =
|
|
TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_forced_break_multi() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello\n\n\nWorld";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines =
|
|
TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 4);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "");
|
|
assert_eq!(lines[2].line_text(text), "");
|
|
assert_eq!(lines[3].line_text(text), "World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_forced_break_multi_char_wrap() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello\n\n\nWorld";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(30.),
|
|
None,
|
|
TextWrap::CharWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 6);
|
|
assert_eq!(lines[0].line_text(text), "Hel");
|
|
assert_eq!(lines[1].line_text(text), "lo");
|
|
assert_eq!(lines[2].line_text(text), "");
|
|
assert_eq!(lines[3].line_text(text), "");
|
|
assert_eq!(lines[4].line_text(text), "Wor");
|
|
assert_eq!(lines[5].line_text(text), "ld");
|
|
}
|
|
|
|
#[test]
|
|
fn test_forced_break_max_lines() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello\n\n\nWorld";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
None,
|
|
Some(2),
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_nbsp_break() {
|
|
let font = FixedTestFont;
|
|
let text = "Ok Hello\u{00a0}World";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(110.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "Ok");
|
|
assert_eq!(lines[1].line_text(text), "Hello\u{00a0}World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_line_multi_break_opportunity() {
|
|
let font = FixedTestFont;
|
|
let text = "a b c";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines =
|
|
TextLineBreaker::<FixedTestFont>::new(text, &shape_buffer, None, None, TextWrap::WordWrap)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 1);
|
|
assert_eq!(lines[0].line_text(text), "a b c");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_anywhere_fallback() {
|
|
let font = FixedTestFont;
|
|
let text = "HelloWorld";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_anywhere_fallback_multi_line() {
|
|
let font = FixedTestFont;
|
|
let text = "HelloWorld\nHelloWorld";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 4);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "World");
|
|
assert_eq!(lines[2].line_text(text), "Hello");
|
|
assert_eq!(lines[3].line_text(text), "World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_anywhere_fallback_multi_line_char_wrap() {
|
|
let font = FixedTestFont;
|
|
let text = "HelloWorld\nHelloWorld";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
None,
|
|
TextWrap::CharWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 4);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "World");
|
|
assert_eq!(lines[2].line_text(text), "Hello");
|
|
assert_eq!(lines[3].line_text(text), "World");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_anywhere_fallback_multi_line_v2() {
|
|
let font = FixedTestFont;
|
|
let text = "HelloW orldHellow";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 4);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "W");
|
|
assert_eq!(lines[2].line_text(text), "orldH");
|
|
assert_eq!(lines[3].line_text(text), "ellow");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_anywhere_fallback_max_lines() {
|
|
let font = FixedTestFont;
|
|
let text = "HelloW orldHellow";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(50.),
|
|
Some(3),
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 3);
|
|
assert_eq!(lines[0].line_text(text), "Hello");
|
|
assert_eq!(lines[1].line_text(text), "W");
|
|
assert_eq!(lines[2].line_text(text), "orldH");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_space() {
|
|
// The available width is half-way into the trailing "W"
|
|
let font = FixedTestFont;
|
|
let text = "H W";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(25.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "H");
|
|
assert_eq!(lines[1].line_text(text), "W");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_space_char_wrap() {
|
|
// The available width is half-way into the trailing "W"
|
|
let font = FixedTestFont;
|
|
let text = "H W";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(25.),
|
|
None,
|
|
TextWrap::CharWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "H");
|
|
assert_eq!(lines[1].line_text(text), "W");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_space_v2() {
|
|
// The available width is half-way into the trailing "W"
|
|
let font = FixedTestFont;
|
|
let text = "B B W";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(45.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "B B");
|
|
assert_eq!(lines[1].line_text(text), "W");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_space_v3() {
|
|
// The available width is half-way into the trailing "W"
|
|
let font = FixedTestFont;
|
|
let text = "H W";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(15.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 2);
|
|
assert_eq!(lines[0].line_text(text), "H");
|
|
assert_eq!(lines[1].line_text(text), "W");
|
|
}
|
|
|
|
#[test]
|
|
fn test_basic_line_break_space_v4() {
|
|
// The available width is half-way into the trailing space
|
|
let font = FixedTestFont;
|
|
let text = "H W H ";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(65.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 1);
|
|
assert_eq!(lines[0].line_text(text), "H W H");
|
|
}
|
|
|
|
#[test]
|
|
fn test_line_width_with_whitespace() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello World";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(200.),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines.len(), 1);
|
|
assert_eq!(lines[0].text_width, text.len() as f32 * 10.);
|
|
}
|
|
|
|
#[test]
|
|
fn zero_width() {
|
|
let font = FixedTestFont;
|
|
let text = "He\nHe o";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(0.0001),
|
|
None,
|
|
TextWrap::WordWrap,
|
|
)
|
|
.map(|t| t.line_text(text))
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines, ["H", "e", "", "H", "e", "o"]);
|
|
}
|
|
|
|
#[test]
|
|
fn zero_width_char_wrap() {
|
|
let font = FixedTestFont;
|
|
let text = "He\nHe o";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(0.0001),
|
|
None,
|
|
TextWrap::CharWrap,
|
|
)
|
|
.map(|t| t.line_text(text))
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines, ["H", "e", "", "H", "e", "o"]);
|
|
}
|
|
|
|
#[test]
|
|
fn char_wrap_sentences() {
|
|
let font = FixedTestFont;
|
|
let text = "Hello world\nHow are you?";
|
|
let shape_buffer = ShapeBuffer::new(
|
|
&TextLayout { font: &font, letter_spacing: None, line_height: None },
|
|
text,
|
|
);
|
|
let lines = TextLineBreaker::<FixedTestFont>::new(
|
|
text,
|
|
&shape_buffer,
|
|
Some(80.),
|
|
None,
|
|
TextWrap::CharWrap,
|
|
)
|
|
.map(|t| t.line_text(text))
|
|
.collect::<std::vec::Vec<_>>();
|
|
assert_eq!(lines, ["Hello wo", "rld", "How are", "you?"]);
|
|
}
|