use crate::{FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString, Size, point, px};
use collections::{FxHashMap, FxHashSet};
use itertools::Itertools;
use smallvec::SmallVec;
use std::{
borrow::Borrow,
cell::RefCell,
hash::{Hash, Hasher},
ops::Range,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use super::LineWrapper;
#[derive(Default, Debug)]
pub struct LineLayout {
pub font_size: Pixels,
pub width: Pixels,
pub ascent: Pixels,
pub descent: Pixels,
pub runs: Vec<ShapedRun>,
pub len: usize,
}
#[derive(Debug, Clone)]
pub struct ShapedRun {
pub font_id: FontId,
pub glyphs: Vec<ShapedGlyph>,
}
#[derive(Clone, Debug)]
pub struct ShapedGlyph {
pub id: GlyphId,
pub position: Point<Pixels>,
pub index: usize,
pub is_emoji: bool,
}
impl LineLayout {
pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
if x >= self.width {
None
} else {
for run in self.runs.iter().rev() {
for glyph in run.glyphs.iter().rev() {
if glyph.position.x <= x {
return Some(glyph.index);
}
}
}
Some(0)
}
}
pub fn closest_index_for_x(&self, x: Pixels) -> usize {
let mut prev_index = 0;
let mut prev_x = px(0.);
for run in self.runs.iter() {
for glyph in run.glyphs.iter() {
if glyph.position.x >= x {
if glyph.position.x - x < x - prev_x {
return glyph.index;
} else {
return prev_index;
}
}
prev_index = glyph.index;
prev_x = glyph.position.x;
}
}
if self.len == 1 {
if x > self.width / 2. {
return 1;
} else {
return 0;
}
}
self.len
}
pub fn x_for_index(&self, index: usize) -> Pixels {
for run in &self.runs {
for glyph in &run.glyphs {
if glyph.index >= index {
return glyph.position.x;
}
}
}
self.width
}
pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
for run in &self.runs {
for glyph in &run.glyphs {
if glyph.index >= index {
return Some(run.font_id);
}
}
}
None
}
pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) {
let x_offset = self.x_for_index(byte_index);
let mut left_runs = Vec::new();
let mut right_runs = Vec::new();
for run in &self.runs {
let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
if split_pos > 0 {
left_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: run.glyphs[..split_pos].to_vec(),
});
}
if split_pos < run.glyphs.len() {
let right_glyphs = run.glyphs[split_pos..]
.iter()
.map(|g| ShapedGlyph {
id: g.id,
position: point(g.position.x - x_offset, g.position.y),
index: g.index - byte_index,
is_emoji: g.is_emoji,
})
.collect();
right_runs.push(ShapedRun {
font_id: run.font_id,
glyphs: right_glyphs,
});
}
}
let left = LineLayout {
font_size: self.font_size,
width: x_offset,
ascent: self.ascent,
descent: self.descent,
runs: left_runs,
len: byte_index,
};
let right = LineLayout {
font_size: self.font_size,
width: self.width - x_offset,
ascent: self.ascent,
descent: self.descent,
runs: right_runs,
len: self.len - byte_index,
};
(left, right)
}
fn compute_wrap_boundaries(
&self,
text: &str,
wrap_width: Pixels,
max_lines: Option<usize>,
scratch: &mut WrapScratch,
) -> SmallVec<[WrapBoundary; 1]> {
let mut boundaries = SmallVec::new();
let mut current_segment: Option<Range<usize>> = None;
let mut last_candidate_ix = None;
let mut last_candidate_x = px(0.);
let mut last_boundary = WrapBoundary {
run_ix: 0,
glyph_ix: 0,
};
let mut last_boundary_x = px(0.);
let WrapScratch {
glyph_indices,
break_indices,
} = scratch;
glyph_indices.clear();
glyph_indices.extend(
self.runs
.iter()
.flat_map(|run| run.glyphs.iter().map(|glyph| glyph.index)),
);
break_indices.clear();
break_indices
.extend(LineWrapper::break_indices(text).filter(|index| glyph_indices.contains(index)));
let mut glyphs = self
.runs
.iter()
.enumerate()
.flat_map(move |(run_ix, run)| {
run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| {
(
WrapBoundary { run_ix, glyph_ix },
glyph.index,
glyph.position.x,
)
})
})
.dedup_by(|(_, previous_index, ..), (_, index, ..)| previous_index == index)
.peekable();
while let Some((boundary, index, x)) = glyphs.next() {
let byte = text.as_bytes()[index];
if byte == b'\n' {
continue;
}
if byte != b' '
&& current_segment
.as_ref()
.is_none_or(|segment| !segment.contains(&index))
{
let segment = break_indices.partition_point(|break_index| *break_index <= index);
if current_segment.is_some() {
last_candidate_ix = Some(boundary);
last_candidate_x = x;
}
let start = segment
.checked_sub(1)
.and_then(|previous| break_indices.get(previous))
.copied()
.unwrap_or(0);
let end = break_indices.get(segment).copied().unwrap_or(text.len());
current_segment = Some(start..end);
}
let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x);
let width = next_x - last_boundary_x;
if width > wrap_width && boundary > last_boundary {
if let Some(max_lines) = max_lines
&& boundaries.len() >= max_lines.saturating_sub(1)
{
break;
}
if let Some(last_candidate_ix) = last_candidate_ix.take() {
last_boundary = last_candidate_ix;
last_boundary_x = last_candidate_x;
} else {
last_boundary = boundary;
last_boundary_x = x;
}
boundaries.push(last_boundary);
}
}
boundaries
}
}
#[derive(Default, Debug)]
pub struct WrappedLineLayout {
pub unwrapped_layout: Arc<LineLayout>,
pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
pub wrap_width: Option<Pixels>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct WrapBoundary {
pub run_ix: usize,
pub glyph_ix: usize,
}
impl WrappedLineLayout {
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.unwrapped_layout.len
}
pub fn width(&self) -> Pixels {
self.wrap_width
.unwrap_or(Pixels::MAX)
.min(self.unwrapped_layout.width)
}
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
Size {
width: self.width(),
height: line_height * (self.wrap_boundaries.len() + 1),
}
}
pub fn ascent(&self) -> Pixels {
self.unwrapped_layout.ascent
}
pub fn descent(&self) -> Pixels {
self.unwrapped_layout.descent
}
pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
&self.wrap_boundaries
}
pub fn font_size(&self) -> Pixels {
self.unwrapped_layout.font_size
}
pub fn runs(&self) -> &[ShapedRun] {
&self.unwrapped_layout.runs
}
pub fn index_for_position(
&self,
position: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize> {
self._index_for_position(position, line_height, false)
}
pub fn closest_index_for_position(
&self,
position: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize> {
self._index_for_position(position, line_height, true)
}
fn _index_for_position(
&self,
position: Point<Pixels>,
line_height: Pixels,
closest: bool,
) -> Result<usize, usize> {
let wrapped_line_ix = (position.y / line_height) as usize;
let wrapped_line_start_index;
let wrapped_line_start_x;
if wrapped_line_ix > 0 {
let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
return Err(0);
};
let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
let glyph = &run.glyphs[line_start_boundary.glyph_ix];
wrapped_line_start_index = glyph.index;
wrapped_line_start_x = glyph.position.x;
} else {
wrapped_line_start_index = 0;
wrapped_line_start_x = Pixels::ZERO;
};
let wrapped_line_end_index;
let wrapped_line_end_x;
if wrapped_line_ix < self.wrap_boundaries.len() {
let next_wrap_boundary_ix = wrapped_line_ix;
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
wrapped_line_end_index = glyph.index;
wrapped_line_end_x = glyph.position.x;
} else {
wrapped_line_end_index = self.unwrapped_layout.len;
wrapped_line_end_x = self.unwrapped_layout.width;
};
let mut position_in_unwrapped_line = position;
position_in_unwrapped_line.x += wrapped_line_start_x;
if position_in_unwrapped_line.x < wrapped_line_start_x {
Err(wrapped_line_start_index)
} else if position_in_unwrapped_line.x >= wrapped_line_end_x {
Err(wrapped_line_end_index)
} else {
if closest {
Ok(self
.unwrapped_layout
.closest_index_for_x(position_in_unwrapped_line.x))
} else {
self.unwrapped_layout
.index_for_x(position_in_unwrapped_line.x)
.ok_or(wrapped_line_end_index)
}
}
}
pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
let mut line_start_ix = 0;
let line_end_indices = self
.wrap_boundaries
.iter()
.map(|wrap_boundary| {
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
let glyph = &run.glyphs[wrap_boundary.glyph_ix];
glyph.index
})
.chain([self.len()])
.enumerate();
for (ix, line_end_ix) in line_end_indices {
let line_y = ix as f32 * line_height;
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_start_ix = line_end_ix;
continue;
} else {
let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
return Some(point(x, line_y));
}
}
None
}
}
pub(crate) struct LineLayoutCache {
state: RefCell<LineLayoutCacheState>,
wrap_scratch: RefCell<WrapScratch>,
platform_text_system: Arc<dyn PlatformTextSystem>,
font_generation: Arc<AtomicUsize>,
cached_font_generation: AtomicUsize,
}
#[derive(Default)]
struct WrapScratch {
glyph_indices: FxHashSet<usize>,
break_indices: Vec<usize>,
}
#[derive(Default)]
struct LineLayoutCacheState {
previous_frame: FrameCache,
current_frame: FrameCache,
}
#[derive(Default)]
struct FrameCache {
lines: FxHashMap<Arc<CacheKey>, Arc<LineLayout>>,
wrapped_lines: FxHashMap<Arc<CacheKey>, Arc<WrappedLineLayout>>,
used_lines: Vec<Arc<CacheKey>>,
used_wrapped_lines: Vec<Arc<CacheKey>>,
}
#[derive(Clone, Default)]
pub(crate) struct LineLayoutIndex {
font_generation: usize,
lines_index: usize,
wrapped_lines_index: usize,
}
impl LineLayoutCache {
pub fn new(
platform_text_system: Arc<dyn PlatformTextSystem>,
font_generation: Arc<AtomicUsize>,
) -> Self {
let cached_font_generation = font_generation.load(Ordering::Acquire);
Self {
state: RefCell::default(),
wrap_scratch: RefCell::default(),
platform_text_system,
font_generation,
cached_font_generation: AtomicUsize::new(cached_font_generation),
}
}
pub fn layout_index(&self) -> LineLayoutIndex {
let font_generation = self.clear_if_font_generation_changed();
let state = self.state.borrow();
let frame = &state.current_frame;
LineLayoutIndex {
font_generation,
lines_index: frame.used_lines.len(),
wrapped_lines_index: frame.used_wrapped_lines.len(),
}
}
pub fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
let font_generation = self.clear_if_font_generation_changed();
if range.start.font_generation != font_generation
|| range.end.font_generation != font_generation
{
return;
}
let mut state = self.state.borrow_mut();
let LineLayoutCacheState {
previous_frame,
current_frame,
} = &mut *state;
for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] {
if let Some((key, line)) = previous_frame.lines.remove_entry(key) {
current_frame.lines.insert(key, line);
}
current_frame.used_lines.push(key.clone());
}
for key in &previous_frame.used_wrapped_lines
[range.start.wrapped_lines_index..range.end.wrapped_lines_index]
{
if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) {
current_frame.wrapped_lines.insert(key, line);
}
current_frame.used_wrapped_lines.push(key.clone());
}
}
pub fn truncate_layouts(&self, index: LineLayoutIndex) {
let font_generation = self.clear_if_font_generation_changed();
if index.font_generation != font_generation {
return;
}
let mut state = self.state.borrow_mut();
let current_frame = &mut state.current_frame;
current_frame.used_lines.truncate(index.lines_index);
current_frame
.used_wrapped_lines
.truncate(index.wrapped_lines_index);
}
pub fn finish_frame(&self) {
let _font_generation = self.clear_if_font_generation_changed();
let LineLayoutCacheState {
previous_frame,
current_frame,
} = &mut *self.state.borrow_mut();
std::mem::swap(previous_frame, current_frame);
current_frame.lines.clear();
current_frame.wrapped_lines.clear();
current_frame.used_lines.clear();
current_frame.used_wrapped_lines.clear();
}
pub fn layout_wrapped_line<Text>(
&self,
text: Text,
font_size: Pixels,
runs: &[FontRun],
wrap_width: Option<Pixels>,
max_lines: Option<usize>,
) -> Arc<WrappedLineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
let _font_generation = self.clear_if_font_generation_changed();
let key = &CacheKeyRef {
text: text.as_ref(),
font_size,
runs,
wrap_width,
force_width: None,
} as &dyn AsCacheKeyRef;
let state = self.state.borrow();
if let Some(layout) = state.current_frame.wrapped_lines.get(key) {
return layout.clone();
}
drop(state);
let mut state = self.state.borrow_mut();
let previous_frame_entry = state.previous_frame.wrapped_lines.remove_entry(key);
if let Some((key, layout)) = previous_frame_entry {
state
.current_frame
.wrapped_lines
.insert(key.clone(), layout.clone());
state.current_frame.used_wrapped_lines.push(key);
layout
} else {
drop(state);
let text = SharedString::from(text);
let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None);
let wrap_boundaries = if let Some(wrap_width) = wrap_width {
unwrapped_layout.compute_wrap_boundaries(
text.as_ref(),
wrap_width,
max_lines,
&mut self.wrap_scratch.borrow_mut(),
)
} else {
SmallVec::new()
};
let layout = Arc::new(WrappedLineLayout {
unwrapped_layout,
wrap_boundaries,
wrap_width,
});
let key = Arc::new(CacheKey {
text,
font_size,
runs: SmallVec::from(runs),
wrap_width,
force_width: None,
});
let mut state = self.state.borrow_mut();
state
.current_frame
.wrapped_lines
.insert(key.clone(), layout.clone());
state.current_frame.used_wrapped_lines.push(key);
layout
}
}
pub fn layout_line<Text>(
&self,
text: Text,
font_size: Pixels,
runs: &[FontRun],
force_width: Option<Pixels>,
) -> Arc<LineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
let _font_generation = self.clear_if_font_generation_changed();
let key = &CacheKeyRef {
text: text.as_ref(),
font_size,
runs,
wrap_width: None,
force_width,
} as &dyn AsCacheKeyRef;
let state = self.state.borrow();
if let Some(layout) = state.current_frame.lines.get(key) {
return layout.clone();
}
drop(state);
let mut state = self.state.borrow_mut();
let LineLayoutCacheState {
previous_frame,
current_frame,
} = &mut *state;
if let Some((key, layout)) = previous_frame.lines.remove_entry(key) {
current_frame.lines.insert(key.clone(), layout.clone());
current_frame.used_lines.push(key);
layout
} else {
let text = SharedString::from(text);
let mut layout = self
.platform_text_system
.layout_line(&text, font_size, runs);
if let Some(force_width) = force_width {
apply_force_width_to_layout(&mut layout, force_width);
}
let key = Arc::new(CacheKey {
text,
font_size,
runs: SmallVec::from(runs),
wrap_width: None,
force_width,
});
let layout = Arc::new(layout);
current_frame.lines.insert(key.clone(), layout.clone());
current_frame.used_lines.push(key);
layout
}
}
fn clear_if_font_generation_changed(&self) -> usize {
let font_generation = self.font_generation.load(Ordering::Acquire);
if self.cached_font_generation.load(Ordering::Acquire) == font_generation {
return font_generation;
}
let mut state = self.state.borrow_mut();
if self.cached_font_generation.load(Ordering::Acquire) == font_generation {
return font_generation;
}
*state = LineLayoutCacheState::default();
self.cached_font_generation
.store(font_generation, Ordering::Release);
font_generation
}
}
fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) {
let mut glyph_pos: usize = 0;
let mut last_base_shaped_x = px(f32::NEG_INFINITY);
let mut last_base_actual_x = px(0.);
for run in layout.runs.iter_mut() {
for glyph in run.glyphs.iter_mut() {
let shaped_x = glyph.position.x;
if shaped_x > last_base_shaped_x + force_width * 0.5 {
let forced_x = glyph_pos * force_width;
if (shaped_x - forced_x).abs() > px(1.) {
glyph.position.x = forced_x;
}
last_base_shaped_x = shaped_x;
last_base_actual_x = glyph.position.x;
glyph_pos += 1;
} else {
glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x);
}
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[expect(missing_docs)]
pub struct FontRun {
pub len: usize,
pub font_id: FontId,
}
trait AsCacheKeyRef {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
}
#[derive(Clone, Debug, Eq)]
struct CacheKey {
text: SharedString,
font_size: Pixels,
runs: SmallVec<[FontRun; 1]>,
wrap_width: Option<Pixels>,
force_width: Option<Pixels>,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct CacheKeyRef<'a> {
text: &'a str,
font_size: Pixels,
runs: &'a [FontRun],
wrap_width: Option<Pixels>,
force_width: Option<Pixels>,
}
impl PartialEq for dyn AsCacheKeyRef + '_ {
fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
self.as_cache_key_ref() == other.as_cache_key_ref()
}
}
impl Eq for dyn AsCacheKeyRef + '_ {}
impl Hash for dyn AsCacheKeyRef + '_ {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_cache_key_ref().hash(state)
}
}
impl AsCacheKeyRef for CacheKey {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
CacheKeyRef {
text: &self.text,
font_size: self.font_size,
runs: self.runs.as_slice(),
wrap_width: self.wrap_width,
force_width: self.force_width,
}
}
}
impl PartialEq for CacheKey {
fn eq(&self, other: &Self) -> bool {
self.as_cache_key_ref().eq(&other.as_cache_key_ref())
}
}
impl Hash for CacheKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_cache_key_ref().hash(state);
}
}
impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
self.as_ref() as &dyn AsCacheKeyRef
}
}
impl AsCacheKeyRef for CacheKeyRef<'_> {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
*self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GlyphId;
fn glyph_at(x: f32, index: usize) -> ShapedGlyph {
ShapedGlyph {
id: GlyphId(0),
position: point(px(x), px(0.)),
index,
is_emoji: false,
}
}
fn make_layout(glyphs: Vec<ShapedGlyph>) -> LineLayout {
LineLayout {
font_size: px(16.),
width: px(100.),
ascent: px(12.),
descent: px(4.),
runs: vec![ShapedRun {
font_id: FontId(0),
glyphs,
}],
len: 0,
}
}
fn glyph_x_positions(layout: &LineLayout) -> Vec<f32> {
layout.runs[0]
.glyphs
.iter()
.map(|g| f32::from(g.position.x))
.collect()
}
#[test]
fn test_wrap_preserves_shaped_clusters() {
let mut layout = make_layout(vec![
glyph_at(0., 0),
glyph_at(5., 0),
glyph_at(10., 3),
glyph_at(15., 3),
]);
layout.width = px(20.);
layout.len = "你好".len();
assert_eq!(
layout
.compute_wrap_boundaries("你好", px(15.), None, &mut WrapScratch::default())
.as_slice(),
&[WrapBoundary {
run_ix: 0,
glyph_ix: 2
}],
);
let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(10., 0)]);
layout.width = px(20.);
layout.len = 1;
assert!(
layout
.compute_wrap_boundaries("a", px(10.), None, &mut WrapScratch::default())
.is_empty()
);
}
#[test]
fn test_wrap_right_to_left_text() {
for (text, indices, expected_boundaries) in [
("אבג דהו", vec![11, 9, 7, 6, 4, 2, 0], vec![4]),
("a אבג b", vec![0, 1, 6, 4, 2, 8, 9], vec![2, 6]),
] {
let mut layout = make_layout(
indices
.iter()
.enumerate()
.map(|(position, index)| glyph_at(position as f32 * 10., *index))
.collect(),
);
layout.width = px(indices.len() as f32 * 10.);
layout.len = text.len();
assert_eq!(
layout.compute_wrap_boundaries(text, px(40.), None, &mut WrapScratch::default()),
expected_boundaries
.into_iter()
.map(|glyph_ix| WrapBoundary {
run_ix: 0,
glyph_ix
})
.collect::<SmallVec<[WrapBoundary; 1]>>(),
"{text:?}",
);
}
}
#[test]
fn test_wrap_ignores_breaks_inside_shaped_clusters() {
let text = "x a-bcde";
let mut layout = make_layout(
[0, 1, 2, 5, 6, 7]
.into_iter()
.map(|index| glyph_at(index as f32 * 10., index))
.collect(),
);
layout.width = px(80.);
layout.len = text.len();
assert_eq!(
layout
.compute_wrap_boundaries(text, px(60.), None, &mut WrapScratch::default())
.as_slice(),
&[WrapBoundary {
run_ix: 0,
glyph_ix: 2
}],
);
}
#[test]
fn test_force_width_latin_unchanged() {
let cell_width = px(8.);
let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]);
apply_force_width_to_layout(&mut layout, cell_width);
let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 8., 16.]);
}
#[test]
fn test_force_width_combining_marks_not_advanced() {
let cell_width = px(8.);
let mut layout = make_layout(vec![
glyph_at(0., 0), glyph_at(0., 3), ]);
apply_force_width_to_layout(&mut layout, cell_width);
let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 0.]);
}
#[test]
fn test_force_width_base_after_combining_mark() {
let cell_width = px(8.);
let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]);
apply_force_width_to_layout(&mut layout, cell_width);
let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 0., 8.]);
}
#[test]
fn test_force_width_multiple_combining_marks() {
let cell_width = px(8.);
let mut layout = make_layout(vec![
glyph_at(0., 0), glyph_at(0., 3), glyph_at(0., 6), glyph_at(8., 9), ]);
apply_force_width_to_layout(&mut layout, cell_width);
let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 0., 0., 8.]);
}
#[test]
fn test_force_width_corrects_drifted_base_positions() {
let cell_width = px(8.);
let mut layout = make_layout(vec![
glyph_at(0.5, 0), glyph_at(10.2, 1), glyph_at(19.8, 2), ]);
apply_force_width_to_layout(&mut layout, cell_width);
let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0.5, 8., 16.]);
}
#[test]
fn test_force_width_combining_mark_after_within_tolerance_base() {
let cell_width = px(8.);
let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]);
apply_force_width_to_layout(&mut layout, cell_width);
let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0.5, 0.5]);
}
}