cosmic-text/src/shape.rs

1713 lines
60 KiB
Rust
Raw Normal View History

2022-10-26 14:16:48 -06:00
// SPDX-License-Identifier: MIT OR Apache-2.0
#![allow(clippy::too_many_arguments)]
2022-11-08 08:43:27 -07:00
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
2023-01-04 20:03:03 -07:00
use core::cmp::{max, min};
use core::fmt;
2022-11-08 08:43:27 -07:00
use core::mem;
2022-12-16 16:49:29 -07:00
use core::ops::Range;
2022-10-26 14:16:48 -06:00
use unicode_script::{Script, UnicodeScript};
use unicode_segmentation::UnicodeSegmentation;
2022-10-26 14:16:48 -06:00
use crate::fallback::FontFallbackIter;
2023-12-18 18:10:09 -05:00
use crate::{
math, Align, AttrsList, CacheKeyFlags, Color, Font, FontSystem, LayoutGlyph, LayoutLine,
2024-06-06 14:40:35 -06:00
Metrics, ShapePlanCache, Wrap,
2023-12-18 18:10:09 -05:00
};
2022-10-26 14:16:48 -06:00
2023-04-21 20:26:08 +02:00
/// The shaping strategy of some text.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Shaping {
2023-04-21 20:26:08 +02:00
/// Basic shaping with no font fallback.
///
/// This shaping strategy is very cheap, but it will not display complex
/// scripts properly nor try to find missing glyphs in your system fonts.
///
/// You should use this strategy when you have complete control of the text
/// and the font you are displaying in your application.
#[cfg(feature = "swash")]
Basic,
2023-04-21 20:26:08 +02:00
/// Advanced text shaping and font fallback.
///
/// You will need to enable this strategy if the text contains a complex
/// script, the font used needs it, and/or multiple fonts in your system
/// may be needed to display all of the glyphs.
Advanced,
}
impl Shaping {
fn run(
self,
scratch: &mut ShapeBuffer,
glyphs: &mut Vec<ShapeGlyph>,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
start_run: usize,
end_run: usize,
span_rtl: bool,
) {
match self {
#[cfg(feature = "swash")]
Self::Basic => shape_skip(font_system, glyphs, line, attrs_list, start_run, end_run),
#[cfg(not(feature = "shape-run-cache"))]
Self::Advanced => shape_run(
scratch,
glyphs,
font_system,
line,
attrs_list,
start_run,
end_run,
span_rtl,
),
#[cfg(feature = "shape-run-cache")]
Self::Advanced => shape_run_cached(
scratch,
glyphs,
font_system,
line,
attrs_list,
start_run,
end_run,
span_rtl,
),
}
}
}
/// A set of buffers containing allocations for shaped text.
#[derive(Default)]
pub struct ShapeBuffer {
/// Buffer for holding unicode text.
rustybuzz_buffer: Option<rustybuzz::UnicodeBuffer>,
/// Temporary buffers for scripts.
scripts: Vec<Script>,
/// Buffer for shape spans.
spans: Vec<ShapeSpan>,
/// Buffer for shape words.
words: Vec<ShapeWord>,
/// Buffers for visual lines.
visual_lines: Vec<VisualLine>,
cached_visual_lines: Vec<VisualLine>,
/// Buffer for sets of layout glyphs.
glyph_sets: Vec<Vec<LayoutGlyph>>,
}
impl fmt::Debug for ShapeBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad("ShapeBuffer { .. }")
}
}
2022-10-26 14:16:48 -06:00
fn shape_fallback(
scratch: &mut ShapeBuffer,
glyphs: &mut Vec<ShapeGlyph>,
2023-12-18 18:10:09 -05:00
shape_plan_cache: &mut ShapePlanCache,
2022-10-26 14:16:48 -06:00
font: &Font,
line: &str,
attrs_list: &AttrsList,
start_run: usize,
end_run: usize,
2022-10-26 14:16:48 -06:00
span_rtl: bool,
) -> Vec<usize> {
let run = &line[start_run..end_run];
2022-10-26 14:16:48 -06:00
2023-03-12 10:23:54 +01:00
let font_scale = font.rustybuzz().units_per_em() as f32;
let ascent = font.rustybuzz().ascender() as f32 / font_scale;
let descent = -font.rustybuzz().descender() as f32 / font_scale;
2022-10-26 14:16:48 -06:00
let mut buffer = scratch.rustybuzz_buffer.take().unwrap_or_default();
2022-10-26 14:16:48 -06:00
buffer.set_direction(if span_rtl {
rustybuzz::Direction::RightToLeft
} else {
rustybuzz::Direction::LeftToRight
});
2024-06-12 10:34:19 -06:00
if run.contains('\t') {
// Push string to buffer, replacing tabs with spaces
//TODO: Find a way to do this with minimal allocating, calling
// UnicodeBuffer::push_str multiple times causes issues and
// UnicodeBuffer::add resizes the buffer with every character
buffer.push_str(&run.replace('\t', " "));
} else {
buffer.push_str(run);
}
2022-10-26 14:16:48 -06:00
buffer.guess_segment_properties();
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
2022-10-26 14:16:48 -06:00
assert_eq!(rtl, span_rtl);
2023-12-18 18:10:09 -05:00
let shape_plan = shape_plan_cache.get(font, &buffer);
let glyph_buffer = rustybuzz::shape_with_plan(font.rustybuzz(), shape_plan, buffer);
2022-10-26 14:16:48 -06:00
let glyph_infos = glyph_buffer.glyph_infos();
let glyph_positions = glyph_buffer.glyph_positions();
let mut missing = Vec::new();
glyphs.reserve(glyph_infos.len());
let glyph_start = glyphs.len();
2022-10-26 14:16:48 -06:00
for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
let x_advance = pos.x_advance as f32 / font_scale;
let y_advance = pos.y_advance as f32 / font_scale;
let x_offset = pos.x_offset as f32 / font_scale;
let y_offset = pos.y_offset as f32 / font_scale;
let start_glyph = start_run + info.cluster as usize;
2022-10-26 14:16:48 -06:00
if info.glyph_id == 0 {
missing.push(start_glyph);
}
let attrs = attrs_list.get_span(start_glyph);
2022-10-26 14:16:48 -06:00
glyphs.push(ShapeGlyph {
start: start_glyph,
end: end_run, // Set later
2022-10-26 14:16:48 -06:00
x_advance,
y_advance,
x_offset,
y_offset,
ascent,
descent,
font_monospace_em_width: font.monospace_em_width(),
2023-03-14 00:39:50 +01:00
font_id: font.id(),
glyph_id: info.glyph_id.try_into().expect("failed to cast glyph ID"),
//TODO: color should not be related to shaping
color_opt: attrs.color_opt,
metadata: attrs.metadata,
2024-01-02 11:36:53 -07:00
cache_key_flags: attrs.cache_key_flags,
2024-06-06 14:40:35 -06:00
metrics_opt: attrs.metrics_opt.map(|x| x.into()),
2022-10-26 14:16:48 -06:00
});
}
// Adjust end of glyphs
if rtl {
for i in glyph_start + 1..glyphs.len() {
2022-10-26 14:16:48 -06:00
let next_start = glyphs[i - 1].start;
let next_end = glyphs[i - 1].end;
let prev = &mut glyphs[i];
if prev.start == next_start {
prev.end = next_end;
} else {
prev.end = next_start;
}
}
} else {
for i in (glyph_start + 1..glyphs.len()).rev() {
2022-10-26 14:16:48 -06:00
let next_start = glyphs[i].start;
let next_end = glyphs[i].end;
let prev = &mut glyphs[i - 1];
if prev.start == next_start {
prev.end = next_end;
} else {
prev.end = next_start;
}
}
}
// Restore the buffer to save an allocation.
scratch.rustybuzz_buffer = Some(glyph_buffer.clear());
missing
2022-10-26 14:16:48 -06:00
}
2023-02-28 19:42:53 +01:00
fn shape_run(
scratch: &mut ShapeBuffer,
glyphs: &mut Vec<ShapeGlyph>,
2023-03-12 10:30:20 +01:00
font_system: &mut FontSystem,
line: &str,
2022-11-04 09:44:54 -06:00
attrs_list: &AttrsList,
start_run: usize,
end_run: usize,
span_rtl: bool,
) {
// Re-use the previous script buffer if possible.
let mut scripts = {
let mut scripts = mem::take(&mut scratch.scripts);
scripts.clear();
scripts
};
for c in line[start_run..end_run].chars() {
match c.script() {
2023-01-04 20:03:03 -07:00
Script::Common | Script::Inherited | Script::Latin | Script::Unknown => (),
script => {
if !scripts.contains(&script) {
scripts.push(script);
}
}
}
}
log::trace!(" Run {:?}: '{}'", &scripts, &line[start_run..end_run],);
let attrs = attrs_list.get_span(start_run);
2023-03-12 10:23:54 +01:00
let fonts = font_system.get_font_matches(attrs);
2023-03-14 00:39:50 +01:00
let default_families = [&attrs.family];
let mut font_iter = FontFallbackIter::new(
font_system,
&fonts,
&default_families,
&scripts,
&line[start_run..end_run],
);
2023-03-12 10:23:54 +01:00
let font = font_iter.next().expect("no default font found");
let glyph_start = glyphs.len();
let mut missing = shape_fallback(
2023-12-18 18:10:09 -05:00
scratch,
glyphs,
font_iter.shape_plan_cache(),
&font,
line,
attrs_list,
start_run,
end_run,
span_rtl,
);
//TODO: improve performance!
while !missing.is_empty() {
let font = match font_iter.next() {
Some(some) => some,
None => break,
};
2023-03-14 00:39:50 +01:00
log::trace!(
"Evaluating fallback with font '{}'",
font_iter.face_name(font.id())
);
let mut fb_glyphs = Vec::new();
let fb_missing = shape_fallback(
scratch,
&mut fb_glyphs,
2023-12-18 18:10:09 -05:00
font_iter.shape_plan_cache(),
&font,
line,
attrs_list,
start_run,
end_run,
span_rtl,
);
// Insert all matching glyphs
let mut fb_i = 0;
while fb_i < fb_glyphs.len() {
let start = fb_glyphs[fb_i].start;
let end = fb_glyphs[fb_i].end;
// Skip clusters that are not missing, or where the fallback font is missing
if !missing.contains(&start) || fb_missing.contains(&start) {
fb_i += 1;
continue;
}
let mut missing_i = 0;
while missing_i < missing.len() {
if missing[missing_i] >= start && missing[missing_i] < end {
// println!("No longer missing {}", missing[missing_i]);
missing.remove(missing_i);
} else {
missing_i += 1;
}
}
// Find prior glyphs
let mut i = glyph_start;
while i < glyphs.len() {
if glyphs[i].start >= start && glyphs[i].end <= end {
break;
} else {
i += 1;
}
}
// Remove prior glyphs
while i < glyphs.len() {
if glyphs[i].start >= start && glyphs[i].end <= end {
let _glyph = glyphs.remove(i);
// log::trace!("Removed {},{} from {}", _glyph.start, _glyph.end, i);
} else {
break;
}
}
while fb_i < fb_glyphs.len() {
if fb_glyphs[fb_i].start >= start && fb_glyphs[fb_i].end <= end {
let fb_glyph = fb_glyphs.remove(fb_i);
// log::trace!("Insert {},{} from font {} at {}", fb_glyph.start, fb_glyph.end, font_i, i);
glyphs.insert(i, fb_glyph);
i += 1;
} else {
break;
}
}
}
}
// Debug missing font fallbacks
font_iter.check_missing(&line[start_run..end_run]);
/*
for glyph in glyphs.iter() {
log::trace!("'{}': {}, {}, {}, {}", &line[glyph.start..glyph.end], glyph.x_advance, glyph.y_advance, glyph.x_offset, glyph.y_offset);
}
*/
// Restore the scripts buffer.
scratch.scripts = scripts;
}
#[cfg(feature = "shape-run-cache")]
fn shape_run_cached(
scratch: &mut ShapeBuffer,
glyphs: &mut Vec<ShapeGlyph>,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
start_run: usize,
end_run: usize,
span_rtl: bool,
) {
use crate::{AttrsOwned, ShapeRunKey};
let run_range = start_run..end_run;
let mut key = ShapeRunKey {
text: line[run_range.clone()].to_string(),
default_attrs: AttrsOwned::new(attrs_list.defaults()),
attrs_spans: Vec::new(),
};
for (attrs_range, attrs) in attrs_list.spans.overlapping(&run_range) {
if attrs == &key.default_attrs {
// Skip if attrs matches default attrs
continue;
}
let start = max(attrs_range.start, start_run)
.checked_sub(start_run)
.unwrap_or(0);
let end = min(attrs_range.end, end_run)
.checked_sub(start_run)
.unwrap_or(0);
if end > start {
let range = start..end;
key.attrs_spans.push((range, attrs.clone()));
}
}
if let Some(cache_glyphs) = font_system.shape_run_cache.get(&key) {
for mut glyph in cache_glyphs.iter().cloned() {
// Adjust glyph start and end to match run position
glyph.start += start_run;
glyph.end += start_run;
glyphs.push(glyph);
}
return;
}
// Fill in cache if not already set
let mut cache_glyphs = Vec::new();
shape_run(
scratch,
&mut cache_glyphs,
font_system,
line,
attrs_list,
start_run,
end_run,
span_rtl,
);
glyphs.extend_from_slice(&cache_glyphs);
for glyph in cache_glyphs.iter_mut() {
// Adjust glyph start and end to remove run position
glyph.start -= start_run;
glyph.end -= start_run;
}
font_system.shape_run_cache.insert(key, cache_glyphs);
}
#[cfg(feature = "swash")]
fn shape_skip(
font_system: &mut FontSystem,
glyphs: &mut Vec<ShapeGlyph>,
line: &str,
attrs_list: &AttrsList,
start_run: usize,
end_run: usize,
) {
let attrs = attrs_list.get_span(start_run);
let fonts = font_system.get_font_matches(attrs);
let default_families = [&attrs.family];
let mut font_iter = FontFallbackIter::new(font_system, &fonts, &default_families, &[], "");
let font = font_iter.next().expect("no default font found");
let font_id = font.id();
let font_monospace_em_width = font.monospace_em_width();
let font = font.as_swash();
let charmap = font.charmap();
let metrics = font.metrics(&[]);
let glyph_metrics = font.glyph_metrics(&[]).scale(1.0);
let ascent = metrics.ascent / f32::from(metrics.units_per_em);
let descent = metrics.descent / f32::from(metrics.units_per_em);
glyphs.extend(
line[start_run..end_run]
.chars()
.enumerate()
.map(|(i, codepoint)| {
let glyph_id = charmap.map(codepoint);
let x_advance = glyph_metrics.advance_width(glyph_id);
let attrs = attrs_list.get_span(i);
ShapeGlyph {
start: i,
end: i + 1,
x_advance,
y_advance: 0.0,
x_offset: 0.0,
y_offset: 0.0,
ascent,
descent,
font_monospace_em_width,
font_id,
glyph_id,
color_opt: attrs.color_opt,
metadata: attrs.metadata,
2024-01-02 11:36:53 -07:00
cache_key_flags: attrs.cache_key_flags,
2024-06-06 14:40:35 -06:00
metrics_opt: attrs.metrics_opt.map(|x| x.into()),
}
}),
);
}
/// A shaped glyph
#[derive(Clone, Debug)]
2022-10-26 14:16:48 -06:00
pub struct ShapeGlyph {
pub start: usize,
pub end: usize,
pub x_advance: f32,
pub y_advance: f32,
pub x_offset: f32,
pub y_offset: f32,
pub ascent: f32,
pub descent: f32,
pub font_monospace_em_width: Option<f32>,
pub font_id: fontdb::ID,
2022-10-26 14:16:48 -06:00
pub glyph_id: u16,
pub color_opt: Option<Color>,
pub metadata: usize,
2024-01-02 11:36:53 -07:00
pub cache_key_flags: CacheKeyFlags,
2024-06-06 14:40:35 -06:00
pub metrics_opt: Option<Metrics>,
2022-10-26 14:16:48 -06:00
}
impl ShapeGlyph {
fn layout(
&self,
font_size: f32,
2024-06-06 15:21:44 -06:00
line_height_opt: Option<f32>,
x: f32,
y: f32,
w: f32,
level: unicode_bidi::Level,
) -> LayoutGlyph {
2022-10-26 14:16:48 -06:00
LayoutGlyph {
start: self.start,
end: self.end,
font_size,
2024-06-06 15:21:44 -06:00
line_height_opt,
font_id: self.font_id,
glyph_id: self.glyph_id,
2022-10-26 14:16:48 -06:00
x,
y,
w,
2022-12-16 16:49:29 -07:00
level,
x_offset: self.x_offset,
y_offset: self.y_offset,
2022-10-26 14:16:48 -06:00
color_opt: self.color_opt,
metadata: self.metadata,
2024-01-02 11:36:53 -07:00
cache_key_flags: self.cache_key_flags,
2022-10-26 14:16:48 -06:00
}
}
2024-06-06 14:40:35 -06:00
2024-06-06 15:37:07 -06:00
/// Get the width of the [`ShapeGlyph`] in pixels, either using the provided font size
/// or the [`ShapeGlyph::metrics_opt`] override.
2024-06-06 14:40:35 -06:00
pub fn width(&self, font_size: f32) -> f32 {
self.metrics_opt.map_or(font_size, |x| x.font_size) * self.x_advance
}
2022-10-26 14:16:48 -06:00
}
/// A shaped word (for word wrapping)
#[derive(Clone, Debug)]
2022-10-26 14:16:48 -06:00
pub struct ShapeWord {
pub blank: bool,
pub glyphs: Vec<ShapeGlyph>,
}
impl ShapeWord {
/// Creates an empty word.
///
/// The returned word is in an invalid state until [`Self::build_in_buffer`] is called.
pub(crate) fn empty() -> Self {
Self {
blank: true,
glyphs: Vec::default(),
}
}
2023-02-28 19:42:53 +01:00
pub fn new(
2023-03-12 10:30:20 +01:00
font_system: &mut FontSystem,
2022-10-26 14:16:48 -06:00
line: &str,
2022-11-04 09:44:54 -06:00
attrs_list: &AttrsList,
2022-12-16 16:49:29 -07:00
word_range: Range<usize>,
level: unicode_bidi::Level,
2022-10-26 14:16:48 -06:00
blank: bool,
shaping: Shaping,
) -> Self {
Self::new_in_buffer(
&mut ShapeBuffer::default(),
font_system,
line,
attrs_list,
word_range,
level,
blank,
shaping,
)
}
/// Shape a word into a set of glyphs, using a scratch buffer.
#[allow(clippy::too_many_arguments)]
pub fn new_in_buffer(
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
word_range: Range<usize>,
level: unicode_bidi::Level,
blank: bool,
shaping: Shaping,
2022-10-26 14:16:48 -06:00
) -> Self {
let mut empty = Self::empty();
empty.build_in_buffer(
scratch,
font_system,
line,
attrs_list,
word_range,
level,
blank,
shaping,
);
empty
}
/// See [`Self::new_in_buffer`].
///
/// Reuses as much of the pre-existing internal allocations as possible.
#[allow(clippy::too_many_arguments)]
pub fn build_in_buffer(
&mut self,
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
word_range: Range<usize>,
level: unicode_bidi::Level,
blank: bool,
shaping: Shaping,
) {
2022-12-16 16:49:29 -07:00
let word = &line[word_range.clone()];
2022-10-26 14:16:48 -06:00
log::trace!(
" Word{}: '{}'",
2022-10-26 14:16:48 -06:00
if blank { " BLANK" } else { "" },
word
2022-10-26 14:16:48 -06:00
);
let mut glyphs = std::mem::take(&mut self.glyphs);
glyphs.clear();
2022-12-16 16:49:29 -07:00
let span_rtl = level.is_rtl();
2022-10-26 14:16:48 -06:00
2022-12-16 16:49:29 -07:00
let mut start_run = word_range.start;
let mut attrs = attrs_list.defaults();
for (egc_i, _egc) in word.grapheme_indices(true) {
2022-12-16 16:49:29 -07:00
let start_egc = word_range.start + egc_i;
let attrs_egc = attrs_list.get_span(start_egc);
2023-01-04 20:03:03 -07:00
if !attrs.compatible(&attrs_egc) {
shaping.run(
scratch,
&mut glyphs,
font_system,
line,
attrs_list,
start_run,
start_egc,
2023-01-04 20:03:03 -07:00
span_rtl,
);
2022-10-26 14:16:48 -06:00
start_run = start_egc;
attrs = attrs_egc;
2022-10-26 14:16:48 -06:00
}
}
2022-12-16 16:49:29 -07:00
if start_run < word_range.end {
shaping.run(
scratch,
&mut glyphs,
font_system,
line,
attrs_list,
start_run,
2022-12-16 16:49:29 -07:00
word_range.end,
2023-01-04 20:03:03 -07:00
span_rtl,
);
2022-10-26 14:16:48 -06:00
}
self.blank = blank;
self.glyphs = glyphs;
2024-06-06 14:40:35 -06:00
}
2022-12-16 16:49:29 -07:00
2024-06-06 15:37:07 -06:00
/// Get the width of the [`ShapeWord`] in pixels, using the [`ShapeGlyph::width`] function.
2024-06-06 14:40:35 -06:00
pub fn width(&self, font_size: f32) -> f32 {
let mut width = 0.0;
for glyph in self.glyphs.iter() {
width += glyph.width(font_size);
2023-01-04 20:03:03 -07:00
}
2024-06-06 14:40:35 -06:00
width
2022-10-26 14:16:48 -06:00
}
}
/// A shaped span (for bidirectional processing)
#[derive(Clone, Debug)]
2022-10-26 14:16:48 -06:00
pub struct ShapeSpan {
2022-12-16 16:49:29 -07:00
pub level: unicode_bidi::Level,
2022-10-26 14:16:48 -06:00
pub words: Vec<ShapeWord>,
}
impl ShapeSpan {
/// Creates an empty span.
///
/// The returned span is in an invalid state until [`Self::build_in_buffer`] is called.
pub(crate) fn empty() -> Self {
Self {
level: unicode_bidi::Level::ltr(),
words: Vec::default(),
}
}
2023-02-28 19:42:53 +01:00
pub fn new(
2023-03-12 10:30:20 +01:00
font_system: &mut FontSystem,
2022-10-26 14:16:48 -06:00
line: &str,
2022-11-04 09:44:54 -06:00
attrs_list: &AttrsList,
2022-12-16 16:49:29 -07:00
span_range: Range<usize>,
2022-10-26 14:16:48 -06:00
line_rtl: bool,
2022-12-16 16:49:29 -07:00
level: unicode_bidi::Level,
shaping: Shaping,
) -> Self {
Self::new_in_buffer(
&mut ShapeBuffer::default(),
font_system,
line,
attrs_list,
span_range,
line_rtl,
level,
shaping,
)
}
/// Shape a span into a set of words, using a scratch buffer.
pub fn new_in_buffer(
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
span_range: Range<usize>,
line_rtl: bool,
level: unicode_bidi::Level,
shaping: Shaping,
2022-10-26 14:16:48 -06:00
) -> Self {
let mut empty = Self::empty();
empty.build_in_buffer(
scratch,
font_system,
line,
attrs_list,
span_range,
line_rtl,
level,
shaping,
);
empty
}
/// See [`Self::new_in_buffer`].
///
/// Reuses as much of the pre-existing internal allocations as possible.
pub fn build_in_buffer(
&mut self,
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
span_range: Range<usize>,
line_rtl: bool,
level: unicode_bidi::Level,
shaping: Shaping,
) {
2022-12-16 16:49:29 -07:00
let span = &line[span_range.start..span_range.end];
2022-10-26 14:16:48 -06:00
log::trace!(
" Span {}: '{}'",
2022-12-16 16:49:29 -07:00
if level.is_rtl() { "RTL" } else { "LTR" },
2022-10-26 14:16:48 -06:00
span
);
let mut words = std::mem::take(&mut self.words);
// Cache the shape words in reverse order so they can be popped for reuse in the same order.
let mut cached_words = std::mem::take(&mut scratch.words);
cached_words.clear();
if line_rtl != level.is_rtl() {
// Un-reverse previous words so the internal glyph counts match accurately when rewriting memory.
cached_words.extend(words.drain(..));
} else {
cached_words.extend(words.drain(..).rev());
}
2022-10-26 14:16:48 -06:00
let mut start_word = 0;
for (end_lb, _) in unicode_linebreak::linebreaks(span) {
let mut start_lb = end_lb;
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
for (i, c) in span[start_word..end_lb].char_indices().rev() {
// TODO: Not all whitespace characters are linebreakable, e.g. 00A0 (No-break
// space)
// https://www.unicode.org/reports/tr14/#GL
// https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt
if c.is_whitespace() {
2022-10-26 14:16:48 -06:00
start_lb = start_word + i;
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
} else {
break;
2022-10-26 14:16:48 -06:00
}
}
if start_word < start_lb {
let mut word = cached_words.pop().unwrap_or_else(ShapeWord::empty);
word.build_in_buffer(
scratch,
2022-10-26 14:16:48 -06:00
font_system,
line,
attrs_list,
2022-12-16 16:49:29 -07:00
(span_range.start + start_word)..(span_range.start + start_lb),
level,
2022-10-26 14:16:48 -06:00
false,
shaping,
);
words.push(word);
2022-10-26 14:16:48 -06:00
}
if start_lb < end_lb {
for (i, c) in span[start_lb..end_lb].char_indices() {
// assert!(c.is_whitespace());
let mut word = cached_words.pop().unwrap_or_else(ShapeWord::empty);
word.build_in_buffer(
scratch,
font_system,
line,
attrs_list,
2023-01-04 20:03:03 -07:00
(span_range.start + start_lb + i)
..(span_range.start + start_lb + i + c.len_utf8()),
level,
true,
shaping,
);
words.push(word);
}
2022-10-26 14:16:48 -06:00
}
start_word = end_lb;
}
// Reverse glyphs in RTL lines
if line_rtl {
for word in &mut words {
2022-10-26 14:16:48 -06:00
word.glyphs.reverse();
}
}
// Reverse words in spans that do not match line direction
2022-12-16 16:49:29 -07:00
if line_rtl != level.is_rtl() {
2022-10-26 14:16:48 -06:00
words.reverse();
}
self.level = level;
self.words = words;
// Cache buffer for future reuse.
scratch.words = cached_words;
2022-10-26 14:16:48 -06:00
}
}
/// A shaped line (or paragraph)
#[derive(Clone, Debug)]
2022-10-26 14:16:48 -06:00
pub struct ShapeLine {
pub rtl: bool,
pub spans: Vec<ShapeSpan>,
pub metrics_opt: Option<Metrics>,
2022-10-26 14:16:48 -06:00
}
// Visual Line Ranges: (span_index, (first_word_index, first_glyph_index), (last_word_index, last_glyph_index))
type VlRange = (usize, (usize, usize), (usize, usize));
2023-03-12 15:33:34 -06:00
#[derive(Default)]
struct VisualLine {
ranges: Vec<VlRange>,
spaces: u32,
w: f32,
}
impl VisualLine {
fn clear(&mut self) {
self.ranges.clear();
self.spaces = 0;
self.w = 0.;
}
}
2022-10-26 14:16:48 -06:00
impl ShapeLine {
/// Creates an empty line.
///
/// The returned line is in an invalid state until [`Self::build_in_buffer`] is called.
pub(crate) fn empty() -> Self {
Self {
rtl: false,
spans: Vec::default(),
metrics_opt: None,
}
}
2023-02-28 19:42:53 +01:00
/// # Panics
///
/// Will panic if `line` contains more than one paragraph.
pub fn new(
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
shaping: Shaping,
2024-06-06 21:01:46 -06:00
tab_width: u16,
) -> Self {
Self::new_in_buffer(
&mut ShapeBuffer::default(),
font_system,
line,
attrs_list,
shaping,
2024-06-06 21:01:46 -06:00
tab_width,
)
}
/// Shape a line into a set of spans, using a scratch buffer. If [`unicode_bidi::BidiInfo`]
/// detects multiple paragraphs, they will be joined.
///
/// # Panics
///
/// Will panic if `line` contains multiple paragraphs that do not have matching direction
pub fn new_in_buffer(
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
shaping: Shaping,
2024-06-06 21:01:46 -06:00
tab_width: u16,
) -> Self {
let mut empty = Self::empty();
empty.build_in_buffer(scratch, font_system, line, attrs_list, shaping, tab_width);
empty
}
/// See [`Self::new_in_buffer`].
///
/// Reuses as much of the pre-existing internal allocations as possible.
pub fn build_in_buffer(
&mut self,
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
line: &str,
attrs_list: &AttrsList,
shaping: Shaping,
tab_width: u16,
) {
let mut spans = std::mem::take(&mut self.spans);
// Cache the shape spans in reverse order so they can be popped for reuse in the same order.
let mut cached_spans = std::mem::take(&mut scratch.spans);
cached_spans.clear();
cached_spans.extend(spans.drain(..).rev());
2022-10-26 14:16:48 -06:00
let bidi = unicode_bidi::BidiInfo::new(line, None);
let rtl = if bidi.paragraphs.is_empty() {
false
} else {
bidi.paragraphs[0].level.is_rtl()
};
2022-10-26 14:16:48 -06:00
log::trace!("Line {}: '{}'", if rtl { "RTL" } else { "LTR" }, line);
for para_info in bidi.paragraphs.iter() {
let line_rtl = para_info.level.is_rtl();
assert_eq!(line_rtl, rtl);
2022-10-26 14:16:48 -06:00
let line_range = para_info.range.clone();
2022-12-16 16:49:29 -07:00
let levels = Self::adjust_levels(&unicode_bidi::Paragraph::new(&bidi, para_info));
2022-10-26 14:16:48 -06:00
2023-01-04 20:03:03 -07:00
// Find consecutive level runs. We use this to create Spans.
2022-12-16 16:49:29 -07:00
// Each span is a set of characters with equal levels.
let mut start = line_range.start;
let mut run_level = levels[start];
spans.reserve(line_range.end - start + 1);
2023-01-04 20:03:03 -07:00
for (i, &new_level) in levels
.iter()
.enumerate()
.take(line_range.end)
.skip(start + 1)
{
2022-12-16 16:49:29 -07:00
if new_level != run_level {
// End of the previous run, start of a new one.
let mut span = cached_spans.pop().unwrap_or_else(ShapeSpan::empty);
span.build_in_buffer(
scratch,
font_system,
line,
attrs_list,
2022-12-16 16:49:29 -07:00
start..i,
2022-10-26 14:16:48 -06:00
line_rtl,
2022-12-16 16:49:29 -07:00
run_level,
shaping,
);
spans.push(span);
2022-12-16 16:49:29 -07:00
start = i;
run_level = new_level;
2022-10-26 14:16:48 -06:00
}
}
let mut span = cached_spans.pop().unwrap_or_else(ShapeSpan::empty);
span.build_in_buffer(
scratch,
2022-12-16 16:49:29 -07:00
font_system,
line,
attrs_list,
start..line_range.end,
line_rtl,
run_level,
shaping,
);
spans.push(span);
}
2022-10-26 14:16:48 -06:00
2024-06-06 21:01:46 -06:00
// Adjust for tabs
let mut x = 0.0;
for span in spans.iter_mut() {
for word in span.words.iter_mut() {
for glyph in word.glyphs.iter_mut() {
if line.get(glyph.start..glyph.end) == Some("\t") {
2024-06-12 10:34:19 -06:00
// Tabs are shaped as spaces, so they will always have the x_advance of a space.
let tab_x_advance = (tab_width as f32) * glyph.x_advance;
2024-06-06 21:01:46 -06:00
let tab_stop = (math::floorf(x / tab_x_advance) + 1.0) * tab_x_advance;
glyph.x_advance = tab_stop - x;
}
x += glyph.x_advance;
}
}
}
self.rtl = rtl;
self.spans = spans;
self.metrics_opt = attrs_list.defaults().metrics_opt.map(|x| x.into());
// Return the buffer for later reuse.
scratch.spans = cached_spans;
2022-10-26 14:16:48 -06:00
}
2022-12-16 16:49:29 -07:00
// A modified version of first part of unicode_bidi::bidi_info::visual_run
2023-01-04 20:03:03 -07:00
fn adjust_levels(para: &unicode_bidi::Paragraph) -> Vec<unicode_bidi::Level> {
2022-12-16 16:49:29 -07:00
use unicode_bidi::BidiClass::*;
let text = para.info.text;
let levels = &para.info.levels;
let original_classes = &para.info.original_classes;
let mut levels = levels.clone();
let line_classes = &original_classes[..];
let line_levels = &mut levels[..];
// Reset some whitespace chars to paragraph level.
// <http://www.unicode.org/reports/tr9/#L1>
let mut reset_from: Option<usize> = Some(0);
let mut reset_to: Option<usize> = None;
2022-12-16 19:15:04 -07:00
for (i, c) in text.char_indices() {
2022-12-16 16:49:29 -07:00
match line_classes[i] {
// Ignored by X9
RLE | LRE | RLO | LRO | PDF | BN => {}
// Segment separator, Paragraph separator
B | S => {
assert_eq!(reset_to, None);
reset_to = Some(i + c.len_utf8());
2022-12-16 19:15:04 -07:00
if reset_from.is_none() {
2022-12-16 16:49:29 -07:00
reset_from = Some(i);
}
}
// Whitespace, isolate formatting
WS | FSI | LRI | RLI | PDI => {
2022-12-16 19:15:04 -07:00
if reset_from.is_none() {
2022-12-16 16:49:29 -07:00
reset_from = Some(i);
}
}
_ => {
reset_from = None;
}
}
if let (Some(from), Some(to)) = (reset_from, reset_to) {
for level in &mut line_levels[from..to] {
*level = para.para.level;
}
reset_from = None;
reset_to = None;
}
}
if let Some(from) = reset_from {
for level in &mut line_levels[from..] {
*level = para.para.level;
}
}
levels
}
// A modified version of second part of unicode_bidi::bidi_info::visual run
fn reorder(&self, line_range: &[VlRange]) -> Vec<Range<usize>> {
2023-01-04 20:03:03 -07:00
let line: Vec<unicode_bidi::Level> = line_range
.iter()
.map(|(span_index, _, _)| self.spans[*span_index].level)
.collect();
2022-12-16 16:49:29 -07:00
// Find consecutive level runs.
let mut runs = Vec::new();
let mut start = 0;
let mut run_level = line[start];
let mut min_level = run_level;
let mut max_level = run_level;
for (i, &new_level) in line.iter().enumerate().skip(start + 1) {
if new_level != run_level {
// End of the previous run, start of a new one.
runs.push(start..i);
start = i;
run_level = new_level;
min_level = min(run_level, min_level);
max_level = max(run_level, max_level);
}
}
runs.push(start..line.len());
let run_count = runs.len();
// Re-order the odd runs.
// <http://www.unicode.org/reports/tr9/#L2>
// Stop at the lowest *odd* level.
min_level = min_level.new_lowest_ge_rtl().expect("Level error");
while max_level >= min_level {
// Look for the start of a sequence of consecutive runs of max_level or higher.
let mut seq_start = 0;
while seq_start < run_count {
if line[runs[seq_start].start] < max_level {
seq_start += 1;
continue;
}
// Found the start of a sequence. Now find the end.
let mut seq_end = seq_start + 1;
while seq_end < run_count {
if line[runs[seq_end].start] < max_level {
break;
}
seq_end += 1;
}
// Reverse the runs within this sequence.
runs[seq_start..seq_end].reverse();
seq_start = seq_end;
}
max_level
.lower(1)
.expect("Lowering embedding level below zero");
}
runs
}
2023-01-04 20:03:03 -07:00
2023-02-22 18:31:49 -07:00
pub fn layout(
&self,
font_size: f32,
width_opt: Option<f32>,
2023-02-22 18:31:49 -07:00
wrap: Wrap,
align: Option<Align>,
match_mono_width: Option<f32>,
2023-02-22 18:31:49 -07:00
) -> Vec<LayoutLine> {
let mut lines = Vec::with_capacity(1);
self.layout_to_buffer(
&mut ShapeBuffer::default(),
font_size,
width_opt,
wrap,
align,
&mut lines,
match_mono_width,
);
lines
}
pub fn layout_to_buffer(
&self,
scratch: &mut ShapeBuffer,
font_size: f32,
width_opt: Option<f32>,
wrap: Wrap,
align: Option<Align>,
layout_lines: &mut Vec<LayoutLine>,
match_mono_width: Option<f32>,
) {
2022-12-16 16:49:29 -07:00
// For each visual line a list of (span index, and range of words in that span)
// Note that a BiDi visual line could have multiple spans or parts of them
2023-02-22 18:31:49 -07:00
// let mut vl_range_of_spans = Vec::with_capacity(1);
let mut visual_lines = mem::take(&mut scratch.visual_lines);
let mut cached_visual_lines = mem::take(&mut scratch.cached_visual_lines);
cached_visual_lines.clear();
cached_visual_lines.extend(visual_lines.drain(..).map(|mut l| {
l.clear();
l
}));
// Cache glyph sets in reverse order so they will ideally be reused in exactly the same lines.
let mut cached_glyph_sets = mem::take(&mut scratch.glyph_sets);
cached_glyph_sets.clear();
cached_glyph_sets.extend(layout_lines.drain(..).rev().map(|mut v| {
v.glyphs.clear();
v.glyphs
}));
2022-10-26 14:16:48 -06:00
2023-03-13 08:50:24 -06:00
fn add_to_visual_line(
vl: &mut VisualLine,
span_index: usize,
start: (usize, usize),
end: (usize, usize),
width: f32,
number_of_blanks: u32,
) {
if end == start {
return;
}
vl.ranges.push((span_index, start, end));
vl.w += width;
vl.spaces += number_of_blanks;
}
2022-12-16 16:49:29 -07:00
// This would keep the maximum number of spans that would fit on a visual line
// If one span is too large, this variable will hold the range of words inside that span
// that fits on a line.
2023-02-22 18:31:49 -07:00
// let mut current_visual_line: Vec<VlRange> = Vec::with_capacity(1);
let mut current_visual_line = cached_visual_lines.pop().unwrap_or_default();
2022-12-19 11:54:19 -07:00
if wrap == Wrap::None {
for (span_index, span) in self.spans.iter().enumerate() {
2023-05-18 22:53:52 +01:00
let mut word_range_width = 0.;
let mut number_of_blanks: u32 = 0;
for word in span.words.iter() {
2024-06-06 14:40:35 -06:00
let word_width = word.width(font_size);
2023-05-18 22:53:52 +01:00
word_range_width += word_width;
if word.blank {
number_of_blanks += 1;
}
}
add_to_visual_line(
&mut current_visual_line,
span_index,
(0, 0),
(span.words.len(), 0),
word_range_width,
number_of_blanks,
);
}
2023-01-04 20:03:03 -07:00
} else {
for (span_index, span) in self.spans.iter().enumerate() {
let mut word_range_width = 0.;
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
let mut width_before_last_blank = 0.;
2023-03-12 15:33:34 -06:00
let mut number_of_blanks: u32 = 0;
// Create the word ranges that fits in a visual line
2023-01-04 20:03:03 -07:00
if self.rtl != span.level.is_rtl() {
// incongruent directions
let mut fitting_start = (span.words.len(), 0);
for (i, word) in span.words.iter().enumerate().rev() {
2024-06-06 14:40:35 -06:00
let word_width = word.width(font_size);
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
// Addition in the same order used to compute the final width, so that
// relayouts with that width as the `line_width` will produce the same
// wrapping results.
if current_visual_line.w + (word_range_width + word_width)
<= width_opt.unwrap_or(f32::INFINITY)
2023-08-25 21:17:56 -04:00
// Include one blank word over the width limit since it won't be
// counted in the final width
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
|| (word.blank
&& (current_visual_line.w + word_range_width) <= width_opt.unwrap_or(f32::INFINITY))
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
{
2023-01-04 20:03:03 -07:00
// fits
2023-02-22 20:48:57 -07:00
if word.blank {
number_of_blanks += 1;
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
width_before_last_blank = word_range_width;
2023-02-22 20:48:57 -07:00
}
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
word_range_width += word_width;
continue;
} else if wrap == Wrap::Glyph
// Make sure that the word is able to fit on it's own line, if not, fall back to Glyph wrapping.
|| (wrap == Wrap::WordOrGlyph && word_width > width_opt.unwrap_or(f32::INFINITY))
{
// Commit the current line so that the word starts on the next line.
if word_range_width > 0.
&& wrap == Wrap::WordOrGlyph
&& word_width > width_opt.unwrap_or(f32::INFINITY)
{
add_to_visual_line(
&mut current_visual_line,
span_index,
(i + 1, 0),
fitting_start,
word_range_width,
number_of_blanks,
);
visual_lines.push(current_visual_line);
current_visual_line = cached_visual_lines.pop().unwrap_or_default();
number_of_blanks = 0;
word_range_width = 0.;
fitting_start = (i, 0);
}
2022-12-19 14:54:23 -07:00
for (glyph_i, glyph) in word.glyphs.iter().enumerate().rev() {
2024-06-06 14:40:35 -06:00
let glyph_width = glyph.width(font_size);
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
if current_visual_line.w + (word_range_width + glyph_width)
<= width_opt.unwrap_or(f32::INFINITY)
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
{
2023-03-12 15:33:34 -06:00
word_range_width += glyph_width;
2022-12-19 14:54:23 -07:00
continue;
} else {
2023-03-13 08:50:24 -06:00
add_to_visual_line(
2023-03-12 15:33:34 -06:00
&mut current_visual_line,
span_index,
2023-01-04 20:03:03 -07:00
(i, glyph_i + 1),
fitting_start,
word_range_width,
2023-02-22 20:48:57 -07:00
number_of_blanks,
2023-03-12 15:33:34 -06:00
);
visual_lines.push(current_visual_line);
current_visual_line =
cached_visual_lines.pop().unwrap_or_default();
2023-03-12 15:33:34 -06:00
2023-02-22 20:48:57 -07:00
number_of_blanks = 0;
2023-03-12 15:33:34 -06:00
word_range_width = glyph_width;
2023-01-04 20:03:03 -07:00
fitting_start = (i, glyph_i + 1);
2022-12-19 14:54:23 -07:00
}
}
2023-01-04 20:03:03 -07:00
} else {
// Wrap::Word, Wrap::WordOrGlyph
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
// If we had a previous range, commit that line before the next word.
if word_range_width > 0. {
// Current word causing a wrap is not whitespace, so we ignore the
// previous word if it's a whitespace
let trailing_blank = span
.words
.get(i + 1)
.map_or(false, |previous_word| previous_word.blank);
if trailing_blank {
number_of_blanks = number_of_blanks.saturating_sub(1);
add_to_visual_line(
&mut current_visual_line,
span_index,
(i + 2, 0),
fitting_start,
width_before_last_blank,
number_of_blanks,
);
} else {
add_to_visual_line(
&mut current_visual_line,
span_index,
(i + 1, 0),
fitting_start,
word_range_width,
number_of_blanks,
);
}
visual_lines.push(current_visual_line);
current_visual_line = cached_visual_lines.pop().unwrap_or_default();
number_of_blanks = 0;
2023-02-22 20:48:57 -07:00
}
2023-03-12 15:33:34 -06:00
2022-12-19 14:54:23 -07:00
if word.blank {
word_range_width = 0.;
fitting_start = (i, 0);
2022-12-19 14:54:23 -07:00
} else {
2023-03-12 15:33:34 -06:00
word_range_width = word_width;
2023-01-04 20:03:03 -07:00
fitting_start = (i + 1, 0);
2022-12-19 14:54:23 -07:00
}
2022-10-26 14:16:48 -06:00
}
}
2023-03-13 08:50:24 -06:00
add_to_visual_line(
2023-03-12 15:33:34 -06:00
&mut current_visual_line,
span_index,
(0, 0),
fitting_start,
word_range_width,
number_of_blanks,
);
2023-01-04 20:03:03 -07:00
} else {
// congruent direction
let mut fitting_start = (0, 0);
for (i, word) in span.words.iter().enumerate() {
2024-06-06 14:40:35 -06:00
let word_width = word.width(font_size);
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
if current_visual_line.w + (word_range_width + word_width)
<= width_opt.unwrap_or(f32::INFINITY)
2023-08-25 21:17:56 -04:00
// Include one blank word over the width limit since it won't be
// counted in the final width.
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
|| (word.blank
&& (current_visual_line.w + word_range_width) <= width_opt.unwrap_or(f32::INFINITY))
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
{
2023-01-04 20:03:03 -07:00
// fits
2023-02-22 20:48:57 -07:00
if word.blank {
number_of_blanks += 1;
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
width_before_last_blank = word_range_width;
2023-02-22 20:48:57 -07:00
}
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
word_range_width += word_width;
continue;
} else if wrap == Wrap::Glyph
// Make sure that the word is able to fit on it's own line, if not, fall back to Glyph wrapping.
|| (wrap == Wrap::WordOrGlyph && word_width > width_opt.unwrap_or(f32::INFINITY))
{
// Commit the current line so that the word starts on the next line.
if word_range_width > 0.
&& wrap == Wrap::WordOrGlyph
&& word_width > width_opt.unwrap_or(f32::INFINITY)
{
add_to_visual_line(
&mut current_visual_line,
span_index,
fitting_start,
(i, 0),
word_range_width,
number_of_blanks,
);
visual_lines.push(current_visual_line);
current_visual_line = cached_visual_lines.pop().unwrap_or_default();
number_of_blanks = 0;
word_range_width = 0.;
fitting_start = (i, 0);
}
2022-12-19 14:54:23 -07:00
for (glyph_i, glyph) in word.glyphs.iter().enumerate() {
2024-06-06 14:40:35 -06:00
let glyph_width = glyph.width(font_size);
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
if current_visual_line.w + (word_range_width + glyph_width)
<= width_opt.unwrap_or(f32::INFINITY)
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
{
2023-03-12 15:33:34 -06:00
word_range_width += glyph_width;
2022-12-19 14:54:23 -07:00
continue;
} else {
2023-03-13 08:50:24 -06:00
add_to_visual_line(
2023-03-12 15:33:34 -06:00
&mut current_visual_line,
span_index,
2023-01-04 20:03:03 -07:00
fitting_start,
(i, glyph_i),
word_range_width,
2023-02-22 20:48:57 -07:00
number_of_blanks,
2023-03-12 15:33:34 -06:00
);
visual_lines.push(current_visual_line);
current_visual_line =
cached_visual_lines.pop().unwrap_or_default();
2023-03-12 15:33:34 -06:00
2023-02-22 20:48:57 -07:00
number_of_blanks = 0;
2023-03-12 15:33:34 -06:00
word_range_width = glyph_width;
2022-12-19 14:54:23 -07:00
fitting_start = (i, glyph_i);
}
}
2023-01-04 20:03:03 -07:00
} else {
// Wrap::Word, Wrap::WordOrGlyph
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
// If we had a previous range, commit that line before the next word.
if word_range_width > 0. {
// Current word causing a wrap is not whitespace, so we ignore the
// previous word if it's a whitespace.
let trailing_blank = i > 0 && span.words[i - 1].blank;
if trailing_blank {
number_of_blanks = number_of_blanks.saturating_sub(1);
add_to_visual_line(
&mut current_visual_line,
span_index,
fitting_start,
(i - 1, 0),
width_before_last_blank,
number_of_blanks,
);
} else {
add_to_visual_line(
&mut current_visual_line,
span_index,
fitting_start,
(i, 0),
word_range_width,
number_of_blanks,
);
}
visual_lines.push(current_visual_line);
current_visual_line = cached_visual_lines.pop().unwrap_or_default();
number_of_blanks = 0;
2023-02-22 20:48:57 -07:00
}
2022-12-19 14:54:23 -07:00
if word.blank {
word_range_width = 0.;
2023-01-04 20:03:03 -07:00
fitting_start = (i + 1, 0);
2022-12-19 14:54:23 -07:00
} else {
2023-03-12 15:33:34 -06:00
word_range_width = word_width;
2022-12-19 14:54:23 -07:00
fitting_start = (i, 0);
}
2022-12-16 21:58:26 -07:00
}
2022-10-26 14:16:48 -06:00
}
2023-03-13 08:50:24 -06:00
add_to_visual_line(
2023-03-12 15:33:34 -06:00
&mut current_visual_line,
span_index,
2023-02-22 20:48:57 -07:00
fitting_start,
(span.words.len(), 0),
word_range_width,
number_of_blanks,
2023-03-12 15:33:34 -06:00
);
}
2022-12-16 16:49:29 -07:00
}
}
2023-02-22 18:31:49 -07:00
if !current_visual_line.ranges.is_empty() {
2023-03-12 15:33:34 -06:00
visual_lines.push(current_visual_line);
} else {
current_visual_line.clear();
cached_visual_lines.push(current_visual_line);
2022-12-16 16:49:29 -07:00
}
2022-10-26 14:16:48 -06:00
2022-12-19 11:54:19 -07:00
// Create the LayoutLines using the ranges inside visual lines
let align = align.unwrap_or({
if self.rtl {
Align::Right
} else {
Align::Left
}
});
let line_width = match width_opt {
Some(width) => width,
None => {
let mut width: f32 = 0.0;
for visual_line in visual_lines.iter() {
width = width.max(visual_line.w);
}
width
}
};
let start_x = if self.rtl { line_width } else { 0.0 };
2023-03-12 15:33:34 -06:00
let number_of_visual_lines = visual_lines.len();
for (index, visual_line) in visual_lines.iter().enumerate() {
if visual_line.ranges.is_empty() {
continue;
}
2023-02-22 18:31:49 -07:00
let new_order = self.reorder(&visual_line.ranges);
let mut glyphs = cached_glyph_sets
.pop()
.unwrap_or_else(|| Vec::with_capacity(1));
let mut x = start_x;
let mut y = 0.;
let mut max_ascent: f32 = 0.;
let mut max_descent: f32 = 0.;
2023-02-22 18:31:49 -07:00
let alignment_correction = match (align, self.rtl) {
(Align::Left, true) => line_width - visual_line.w,
2023-02-22 18:31:49 -07:00
(Align::Left, false) => 0.,
(Align::Right, true) => 0.,
(Align::Right, false) => line_width - visual_line.w,
(Align::Center, _) => (line_width - visual_line.w) / 2.0,
(Align::End, _) => line_width - visual_line.w,
(Align::Justified, _) => 0.,
2023-02-22 18:31:49 -07:00
};
2022-12-16 16:49:29 -07:00
if self.rtl {
x -= alignment_correction;
} else {
x += alignment_correction;
}
// TODO: Only certain `is_whitespace` chars are typically expanded but this is what is
// currently used to compute `visual_line.spaces`.
//
// https://www.unicode.org/reports/tr14/#Introduction
// > When expanding or compressing interword space according to common
// > typographical practice, only the spaces marked by U+0020 SPACE and U+00A0
// > NO-BREAK SPACE are subject to compression, and only spaces marked by U+0020
// > SPACE, U+00A0 NO-BREAK SPACE, and occasionally spaces marked by U+2009 THIN
// > SPACE are subject to expansion. All other space characters normally have
// > fixed width.
//
// (also some spaces aren't followed by potential linebreaks but they could
// still be expanded)
// Amount of extra width added to each blank space within a line.
let justification_expansion = if matches!(align, Align::Justified)
&& visual_line.spaces > 0
// Don't justify the last line in a paragraph.
&& index != number_of_visual_lines - 1
{
(line_width - visual_line.w) / visual_line.spaces as f32
} else {
0.
};
let mut process_range = |range: Range<usize>| {
for &(span_index, (starting_word, starting_glyph), (ending_word, ending_glyph)) in
visual_line.ranges[range.clone()].iter()
{
let span = &self.spans[span_index];
// If ending_glyph is not 0 we need to include glyphs from the ending_word
for i in starting_word..ending_word + usize::from(ending_glyph != 0) {
let word = &span.words[i];
let included_glyphs = match (i == starting_word, i == ending_word) {
(false, false) => &word.glyphs[..],
(true, false) => &word.glyphs[starting_glyph..],
(false, true) => &word.glyphs[..ending_glyph],
(true, true) => &word.glyphs[starting_glyph..ending_glyph],
};
for glyph in included_glyphs {
2024-06-06 14:40:35 -06:00
// Use overridden font size
let font_size = glyph.metrics_opt.map_or(font_size, |x| x.font_size);
let match_mono_em_width = match_mono_width.map(|w| w / font_size);
2024-01-17 13:26:39 -07:00
let glyph_font_size = match (
match_mono_em_width,
glyph.font_monospace_em_width,
) {
(Some(match_em_width), Some(glyph_em_width))
if glyph_em_width != match_em_width =>
{
let glyph_to_match_factor = glyph_em_width / match_em_width;
let glyph_font_size = math::roundf(glyph_to_match_factor)
.max(1.0)
/ glyph_to_match_factor
* font_size;
log::trace!("Adjusted glyph font size ({font_size} => {glyph_font_size})");
glyph_font_size
2024-01-17 13:26:39 -07:00
}
_ => font_size,
};
let x_advance = glyph_font_size * glyph.x_advance
+ if word.blank {
justification_expansion
} else {
0.0
};
if self.rtl {
x -= x_advance;
2023-09-23 17:13:03 +03:00
}
let y_advance = glyph_font_size * glyph.y_advance;
2024-06-06 15:21:44 -06:00
glyphs.push(glyph.layout(
glyph_font_size,
glyph.metrics_opt.map(|x| x.line_height),
x,
y,
x_advance,
span.level,
));
2023-09-23 17:13:03 +03:00
if !self.rtl {
x += x_advance;
2022-12-19 14:54:23 -07:00
}
y += y_advance;
2024-06-06 14:40:35 -06:00
max_ascent = max_ascent.max(glyph_font_size * glyph.ascent);
max_descent = max_descent.max(glyph_font_size * glyph.descent);
2022-10-26 14:16:48 -06:00
}
}
}
};
if self.rtl {
for range in new_order.into_iter().rev() {
process_range(range);
}
} else {
/* LTR */
2022-12-16 16:49:29 -07:00
for range in new_order {
process_range(range);
2022-10-26 14:16:48 -06:00
}
}
let mut line_height_opt: Option<f32> = None;
for glyph in glyphs.iter() {
if let Some(glyph_line_height) = glyph.line_height_opt {
line_height_opt = match line_height_opt {
Some(line_height) => Some(line_height.max(glyph_line_height)),
None => Some(glyph_line_height),
};
}
}
2023-01-04 20:03:03 -07:00
layout_lines.push(LayoutLine {
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
w: if align != Align::Justified {
visual_line.w
2023-11-15 09:21:13 -07:00
} else if self.rtl {
start_x - x
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
} else {
2023-11-15 09:21:13 -07:00
x
Fix #134 and include a test for it. Try to ensure that using "the width computed during an unconstrained layout" as the width constraint during a relayout produces the same layout. This is useful for certain UI layout algorithms. See https://github.com/pop-os/cosmic-text/issues/134 * Instead of computing the LayoutLine width from the positioned and aligned glyphs, we pass through width computed during line wrapping (unless justified alignment is used, in this case we use the old approach because the use case for measuring the width isn't really applicable to justified text since that will just expand to the provided width). For the produced width to later give the same wrapping results when passed in as the `line_width` it needs to use the same exact float arithmatic that was used to compute the width that is compared against `line_width` when making line wrapping choices. Passing this width through as the LayoutLine width is the most covenient option without making more major changse to the API. Nevertheless, I am imagining that if we get a dedicated measurement method (i.e. that doesn't do the final positioning and alignment of glyphs and which caches `Vec<VisualLine>`), then this width can just be exposed there instead of preservering it in LayoutLine. * Incidentally, this fixes https://github.com/pop-os/cosmic-text/issues/169. * Switch substraction from `fit_x` to checking whether potential addition to the current line width would exceed the `line_width`. This avoids the float error being dependent on the provided `line_width` value. * When eliminating trailing space from the line width, we avoid backtracking with subtraction (which would not give the same exact value due to float error) and instead save the previous width and use that. * If the previous word did not exceed the line_width, we now include a single blank word even if it would cross the width limit since its width won't be counted. This is necessary to get the same wrapping behavior when re-using the measured width (which doesn't count a single trailing blank word). Note, this whitespace logic may be reworked anyway if <https://github.com/pop-os/cosmic-text/issues/155> is addressed. * Change tests to use `opt-level=1` to keep test runtime down. * Add `fonts` folder for fonts used in tests. * Fix an issue where a non-breaking whitespace was assumed to be the start of a section of spaces which included characters that weren't even whitespace. * Add some TODOs about incongruencies between `is_whitespace`, justification, and line breaks.
2023-08-24 22:37:32 -04:00
},
2024-06-06 14:40:35 -06:00
max_ascent,
max_descent,
line_height_opt,
glyphs,
2023-01-04 20:03:03 -07:00
});
2022-12-16 16:49:29 -07:00
}
// This is used to create a visual line for empty lines (e.g. lines with only a <CR>)
if layout_lines.is_empty() {
2023-01-04 20:03:03 -07:00
layout_lines.push(LayoutLine {
w: 0.0,
max_ascent: 0.0,
max_descent: 0.0,
line_height_opt: self.metrics_opt.map(|x| x.line_height),
2023-01-04 20:03:03 -07:00
glyphs: Default::default(),
});
2022-10-26 14:16:48 -06:00
}
// Restore the buffer to the scratch set to prevent reallocations.
scratch.visual_lines = visual_lines;
scratch.visual_lines.extend(cached_visual_lines.drain(..));
scratch.cached_visual_lines = cached_visual_lines;
scratch.glyph_sets = cached_glyph_sets;
2022-10-26 14:16:48 -06:00
}
}