use std::cell::Cell;
use slate_text::error::TextError;
use slate_text::font_handle::FontHandle;
use slate_text::types::{
FontDescriptor, FontId, FontMetrics, GlyphBitmap, GlyphBounds, ShapedGlyph, ShapedLine,
};
use slate_text::{Font, TextBackend, shape_document, shape_words, wrap_document};
struct MockFont {
handle: FontHandle,
metrics: FontMetrics,
}
impl Font for MockFont {
fn handle(&self) -> FontHandle {
self.handle
}
fn metrics(&self) -> FontMetrics {
self.metrics
}
fn size_lpx(&self) -> f32 {
16.0
}
fn scale(&self) -> f32 {
1.0
}
}
struct MockBackend;
impl MockBackend {
fn font() -> MockFont {
MockFont {
handle: FontHandle::from_face_id(0x1000, 16.0, 1.0),
metrics: FontMetrics {
ascent_lpx: 12.0,
descent_lpx: -4.0,
line_gap_lpx: 2.0,
x_height_lpx: 8.0,
cap_height_lpx: 10.0,
units_per_em: 2048,
},
}
}
}
fn shape_line_impl(font: &MockFont, text: &str) -> ShapedLine {
let mut pen = 0.0f32;
let mut byte = 0usize;
let glyphs: Vec<ShapedGlyph> = text
.chars()
.map(|c| {
let advance = if c == ' ' { 5.0 } else { 10.0 };
let g = ShapedGlyph {
glyph_id: byte as u32,
font_id: FontId::PRIMARY,
font_handle: Default::default(),
x_advance_lpx: advance,
position_lpx: [pen, 0.0],
cluster: byte as u32,
direction: slate_text::Direction::Ltr,
};
pen += advance;
byte += c.len_utf8();
g
})
.collect();
let width: f32 = glyphs.iter().map(|g| g.x_advance_lpx).sum();
ShapedLine {
glyphs,
width_lpx: width,
ascent_lpx: font.metrics.ascent_lpx,
descent_lpx: font.metrics.descent_lpx,
y_offset_lpx: 0.0,
base_direction: slate_text::Direction::Ltr,
runs: Vec::new(),
}
}
impl TextBackend for MockBackend {
type Font = MockFont;
fn load_font(&mut self, _f: &str, _s: f32, _sc: f32) -> Result<Self::Font, TextError> {
Ok(MockBackend::font())
}
fn load_font_from_bytes(
&mut self,
_b: &'static [u8],
_s: f32,
_sc: f32,
) -> Result<Self::Font, TextError> {
Ok(MockBackend::font())
}
fn shape_line(&self, font: &Self::Font, text: &str) -> Result<ShapedLine, TextError> {
Ok(shape_line_impl(font, text))
}
fn rasterize_glyph(&self, _f: &Self::Font, _g: u32, _v: u8) -> Result<GlyphBitmap, TextError> {
Ok(GlyphBitmap {
width: 8,
height: 12,
bearing_x_lpx: 1.0,
bearing_y_lpx: 10.0,
advance_x_lpx: 10.0,
alpha: vec![0xFF; 96],
})
}
fn glyph_raster_bounds(&self, _f: &Self::Font, _g: u32) -> Result<GlyphBounds, TextError> {
Ok(GlyphBounds {
width: 8,
height: 12,
})
}
fn enumerate_system_fonts(&self) -> Result<Vec<FontDescriptor>, TextError> {
Ok(vec![])
}
}
struct CountingBackend {
calls: Cell<usize>,
}
impl TextBackend for CountingBackend {
type Font = MockFont;
fn load_font(&mut self, _f: &str, _s: f32, _sc: f32) -> Result<Self::Font, TextError> {
Ok(MockBackend::font())
}
fn load_font_from_bytes(
&mut self,
_b: &'static [u8],
_s: f32,
_sc: f32,
) -> Result<Self::Font, TextError> {
Ok(MockBackend::font())
}
fn shape_line(&self, font: &Self::Font, text: &str) -> Result<ShapedLine, TextError> {
self.calls.set(self.calls.get() + 1);
Ok(shape_line_impl(font, text))
}
fn rasterize_glyph(&self, _f: &Self::Font, _g: u32, _v: u8) -> Result<GlyphBitmap, TextError> {
Ok(GlyphBitmap {
width: 8,
height: 12,
bearing_x_lpx: 1.0,
bearing_y_lpx: 10.0,
advance_x_lpx: 10.0,
alpha: vec![0xFF; 96],
})
}
fn glyph_raster_bounds(&self, _f: &Self::Font, _g: u32) -> Result<GlyphBounds, TextError> {
Ok(GlyphBounds {
width: 8,
height: 12,
})
}
fn enumerate_system_fonts(&self) -> Result<Vec<FontDescriptor>, TextError> {
Ok(vec![])
}
}
const LINE_HEIGHT: f32 = 18.0;
#[test]
fn shape_words_records_source_byte_ranges() {
let mut backend = MockBackend;
let font = backend.load_font("mock", 16.0, 1.0).unwrap();
let (items, _) = shape_words(&backend, &font, "ab cd").unwrap();
assert_eq!(items.len(), 3);
assert_eq!(items[0].source_byte_range, 0..2);
assert!(!items[0].is_space_run);
assert!(items[1].is_space_run);
assert_eq!(items[1].source_byte_range, 2..3);
assert_eq!(items[2].source_byte_range, 3..5);
assert!(!items[2].is_space_run);
let (items, _) = shape_words(&backend, &font, "你 好").unwrap();
assert_eq!(items[0].source_byte_range, 0..3);
assert_eq!(items[2].source_byte_range, 4..7);
let (items, _) = shape_words(&backend, &font, "😀 x").unwrap();
assert_eq!(items[0].source_byte_range, 0..4);
assert_eq!(items[2].source_byte_range, 5..6);
}
#[test]
fn wrap_yields_contiguous_byte_ranges() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "aa bb cc"; let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 50.0);
assert_eq!(layout.lines.len(), 2);
assert_eq!(layout.lines[0].byte_start, 0);
assert_eq!(layout.lines[0].byte_end, 6); assert_eq!(layout.lines[1].byte_start, 6);
assert_eq!(layout.lines[1].byte_end, text.len());
assert_eq!(layout.lines[0].byte_end, layout.lines[1].byte_start);
assert_eq!(layout.lines[0].byte_start, 0);
assert_eq!(layout.lines.last().unwrap().byte_end, text.len());
}
#[test]
fn hard_newline_splits_lines() {
let backend = MockBackend;
let font = MockBackend::font();
let doc = shape_document(&backend, &font, "a\nb").unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.lines.len(), 2);
assert_eq!(layout.lines[0].byte_start, 0);
assert_eq!(layout.lines[0].byte_end, 2); assert_eq!(layout.lines[1].byte_start, 2);
assert_eq!(layout.lines[1].byte_end, 3);
assert_eq!(layout.lines[0].line.y_offset_lpx, 0.0);
assert_eq!(layout.lines[1].line.y_offset_lpx, LINE_HEIGHT);
}
#[test]
fn empty_paragraph_is_full_height_blank_line() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "a\n\nb"; let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.lines.len(), 3);
assert!(layout.lines[1].line.glyphs.is_empty());
assert_eq!(layout.lines[1].byte_start, 2);
assert_eq!(layout.lines[1].byte_end, 3);
assert!(layout.lines[0].line.y_offset_lpx < layout.lines[1].line.y_offset_lpx);
assert!(layout.lines[1].line.y_offset_lpx < layout.lines[2].line.y_offset_lpx);
assert_eq!(layout.lines[0].byte_start, 0);
assert_eq!(layout.lines.last().unwrap().byte_end, text.len());
}
#[test]
fn total_height_is_sum_of_line_heights() {
let backend = MockBackend;
let font = MockBackend::font();
let doc = shape_document(&backend, &font, "aa bb cc").unwrap();
let layout = wrap_document(&doc, 50.0);
assert_eq!(layout.line_height_lpx, LINE_HEIGHT);
assert_eq!(
layout.total_height_lpx,
layout.lines.len() as f32 * LINE_HEIGHT
);
assert_eq!(layout.total_height_lpx, 2.0 * LINE_HEIGHT);
}
#[test]
fn rewrap_at_new_width_does_no_reshaping() {
let backend = CountingBackend {
calls: Cell::new(0),
};
let font = MockBackend::font();
let doc = shape_document(&backend, &font, "Hello world test again").unwrap();
let after_shape = backend.calls.get();
assert!(after_shape > 0, "shaping should call shape_line");
let lines_a = wrap_document(&doc, 80.0);
assert_eq!(
backend.calls.get(),
after_shape,
"first wrap must add zero shape_line calls"
);
let lines_b = wrap_document(&doc, 40.0);
assert_eq!(
backend.calls.get(),
after_shape,
"re-wrap at a new width must add zero shape_line calls"
);
assert!(lines_b.lines.len() >= lines_a.lines.len());
}
#[test]
fn multi_space_run_contributes_to_line_width() {
let backend = MockBackend;
let font = MockBackend::font();
let doc = shape_document(&backend, &font, "a b").unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.lines.len(), 1);
assert!(
(layout.lines[0].line.width_lpx - 45.0).abs() < 0.01,
"width should include all 5 spaces: {}",
layout.lines[0].line.width_lpx
);
}
#[test]
fn multi_space_wraps_at_run_boundary_and_absorbs_trailing() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "aaaa bbbb";
let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 60.0);
assert_eq!(layout.lines.len(), 2);
assert!(
(layout.lines[0].line.width_lpx - 40.0).abs() < 0.01,
"line0 must not count absorbed trailing spaces: {}",
layout.lines[0].line.width_lpx
);
assert!(
(layout.lines[1].line.width_lpx - 40.0).abs() < 0.01,
"line1 (bbbb) must start at x0, no leading spaces: {}",
layout.lines[1].line.width_lpx
);
assert_eq!(layout.lines[0].byte_start, 0);
assert_eq!(layout.lines[0].byte_end, layout.lines[1].byte_start);
assert_eq!(layout.lines.last().unwrap().byte_end, text.len());
}
#[test]
fn caret_advances_by_space_width_through_run() {
let backend = MockBackend;
let font = MockBackend::font();
let doc = shape_document(&backend, &font, "a b").unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.caret_position(1), (0, 10.0, 0.0));
assert_eq!(layout.caret_position(2), (0, 15.0, 0.0));
assert_eq!(layout.caret_position(3), (0, 20.0, 0.0));
assert_eq!(layout.caret_position(4), (0, 25.0, 0.0));
assert_eq!(layout.caret_position(5), (0, 30.0, 0.0));
assert_eq!(layout.caret_position(6), (0, 35.0, 0.0));
assert_eq!(layout.caret_position(7), (0, 45.0, 0.0));
}
#[test]
fn byte_at_line_x_inside_space_run() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "a b";
let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.byte_at_line_x(text, 0, 18.0), 3);
assert_eq!(layout.byte_at_line_x(text, 0, 11.0), 1);
}
#[test]
fn pure_leading_whitespace_is_addressable() {
let backend = MockBackend;
let font = MockBackend::font();
let text = " "; let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.lines.len(), 1);
assert!((layout.lines[0].line.width_lpx - 25.0).abs() < 0.01);
assert_eq!(layout.caret_position(0), (0, 0.0, 0.0));
assert_eq!(layout.caret_position(3), (0, 15.0, 0.0));
assert_eq!(layout.caret_position(5), (0, 25.0, 0.0));
}
#[test]
fn over_width_word_breaks_at_grapheme_boundaries() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "aaaaa"; let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 25.0);
assert!(
layout.lines.len() >= 2,
"over-wide word must break into >=2 lines"
);
for line in &layout.lines {
assert!(
line.line.width_lpx <= 25.0,
"no broken piece may exceed max_width: {}",
line.line.width_lpx
);
}
assert_eq!(layout.lines[0].byte_start, 0);
assert_eq!(layout.lines.last().unwrap().byte_end, text.len());
for w in layout.lines.windows(2) {
assert_eq!(w[0].byte_end, w[1].byte_start);
}
}
#[test]
fn line_for_byte_at_wrap_boundary_picks_next_line() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "aa bb cc";
let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 50.0);
assert_eq!(layout.line_for_byte(0), 0);
assert_eq!(layout.line_for_byte(5), 0);
assert_eq!(layout.line_for_byte(6), 1);
assert_eq!(layout.line_for_byte(8), 1);
}
#[test]
fn crlf_no_stray_glyph_and_caret_end_after_b() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "ab\r\ncd"; let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.lines.len(), 2, "CRLF must hard-break into two lines");
for vline in &layout.lines {
for g in &vline.line.glyphs {
assert!(
g.cluster != 2 && g.cluster != 3,
"a glyph was keyed to a CRLF terminator byte ({})",
g.cluster
);
}
}
assert_eq!(layout.line_caret_end(text, 0), 2);
assert_eq!(
(layout.lines[0].byte_start, layout.lines[0].byte_end),
(0, 4)
);
assert_eq!(
(layout.lines[1].byte_start, layout.lines[1].byte_end),
(4, 6)
);
}
#[test]
fn unicode_separator_caret_end_excludes_separator() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "ab\u{2028}cd"; let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 1000.0);
assert_eq!(layout.lines.len(), 2);
assert_eq!(layout.line_caret_end(text, 0), 2); assert_eq!(layout.lines[1].byte_start, "ab\u{2028}".len());
assert_eq!(layout.lines[1].byte_end, text.len());
}
#[test]
fn crlf_caret_roundtrip() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "ab\r\ncd";
let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 1000.0);
for &(line_idx, byte) in &[(0usize, 0usize), (0, 1), (0, 2), (1, 4), (1, 5), (1, 6)] {
let (_, x, _) = layout.caret_position(byte);
assert_eq!(
layout.byte_at_line_x(text, line_idx, x),
byte,
"x={x} on line {line_idx} should map back to byte {byte}"
);
}
}
#[test]
fn caret_position_maps_byte_to_line_x_y() {
let backend = MockBackend;
let font = MockBackend::font();
let text = "aa bb cc"; let doc = shape_document(&backend, &font, text).unwrap();
let layout = wrap_document(&doc, 50.0);
assert_eq!(layout.caret_position(0), (0, 0.0, 0.0));
assert_eq!(layout.caret_position(2), (0, 20.0, 0.0));
assert_eq!(layout.caret_position(6), (1, 0.0, LINE_HEIGHT));
assert_eq!(layout.caret_position(8), (1, 20.0, LINE_HEIGHT));
}