use std::rc::Rc;
use slate_text::{MultilineLayout, ShapedLine, TextError};
use crate::text_system::{PlatformFont, TextSystem};
pub(crate) fn build_layout(
text_system: &TextSystem,
font: &PlatformFont,
text: &str,
width_lpx: f32,
min_lines: usize,
) -> Result<(Rc<MultilineLayout>, f32), TextError> {
let doc = text_system.shape_document(font, text)?;
let layout = slate_text::wrap_document(&doc, width_lpx);
let floor_rows = min_lines.max(1) as f32;
let height = layout
.total_height_lpx
.max(floor_rows * layout.line_height_lpx);
Ok((Rc::new(layout), height))
}
pub(crate) fn byte_at_point(
layout: &MultilineLayout,
text: &str,
paint_origin_x: f32,
paint_origin_y: f32,
x: f32,
y: f32,
) -> usize {
if layout.lines.is_empty() {
return 0;
}
let local_y = y - paint_origin_y;
if local_y < 0.0 {
return 0; }
if local_y >= layout.total_height_lpx {
let last = layout.lines.len() - 1;
return layout.line_caret_end(text, last);
}
let lh = layout.line_height_lpx;
let line_idx = if lh > 0.0 {
((local_y / lh) as usize).min(layout.lines.len() - 1)
} else {
0
};
let local_x = x - paint_origin_x;
layout.byte_at_line_x(text, line_idx, local_x)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct SelectionRect {
pub x_lpx: f32,
pub y_lpx: f32,
pub width_lpx: f32,
}
pub(crate) fn selection_rects(
layout: &MultilineLayout,
lo: usize,
hi: usize,
content_width: f32,
) -> Vec<SelectionRect> {
let mut rects = Vec::new();
if lo >= hi {
return rects;
}
for vline in &layout.lines {
if vline.byte_end <= lo || vline.byte_start >= hi {
continue;
}
if !vline.line.runs.is_empty() {
let line_lo = lo.max(vline.byte_start);
let line_hi = hi.min(vline.byte_end);
for (x_start, width) in slate_text::run_selection_rects(&vline.line, line_lo, line_hi) {
rects.push(SelectionRect {
x_lpx: x_start,
y_lpx: vline.line.y_offset_lpx,
width_lpx: width,
});
}
continue;
}
let line_lo = lo.max(vline.byte_start);
let x_start = line_x_at_byte(&vline.line, line_lo);
let starts_at_head = lo <= vline.byte_start;
let covers_to_end = hi >= vline.byte_end;
let x_end = if starts_at_head && covers_to_end {
content_width
} else {
line_x_at_byte(&vline.line, hi.min(vline.byte_end))
};
let width = (x_end - x_start).max(0.0);
if width > 0.0 {
rects.push(SelectionRect {
x_lpx: x_start,
y_lpx: vline.line.y_offset_lpx,
width_lpx: width,
});
}
}
rects
}
pub(crate) fn compose_display(committed: &str, caret: usize, preedit: &str) -> String {
if preedit.is_empty() {
return committed.to_string();
}
let mut at = caret.min(committed.len());
while at > 0 && !committed.is_char_boundary(at) {
at -= 1;
}
let mut out = String::with_capacity(committed.len() + preedit.len());
out.push_str(&committed[..at]);
out.push_str(preedit);
out.push_str(&committed[at..]);
out
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct PreeditRun {
pub line_idx: usize,
pub x_lpx: f32,
pub width_lpx: f32,
}
pub(crate) fn preedit_runs(layout: &MultilineLayout, start: usize, end: usize) -> Vec<PreeditRun> {
let mut runs = Vec::new();
if start >= end {
return runs;
}
for (idx, vline) in layout.lines.iter().enumerate() {
if vline.byte_end <= start || vline.byte_start >= end {
continue;
}
let lo = start.max(vline.byte_start);
let hi = end.min(vline.byte_end);
let x_start = line_x_at_byte(&vline.line, lo);
let x_end = line_x_at_byte(&vline.line, hi);
let width = (x_end - x_start).max(0.0);
if width > 0.0 {
runs.push(PreeditRun {
line_idx: idx,
x_lpx: x_start,
width_lpx: width,
});
}
}
runs
}
fn line_x_at_byte(line: &ShapedLine, byte: usize) -> f32 {
if !line.runs.is_empty() {
return slate_text::run_caret_x_at(line, byte);
}
let mut pen = 0.0f32;
for g in &line.glyphs {
if g.cluster as usize >= byte {
return pen;
}
pen += g.x_advance_lpx;
}
line.width_lpx
}
#[cfg(test)]
mod tests {
use super::*;
use slate_text::{Direction, FontHandle, FontId, ShapedGlyph, VisualLine};
fn glyph(cluster: u32, adv: f32) -> ShapedGlyph {
ShapedGlyph {
glyph_id: 1,
font_id: FontId::PRIMARY,
font_handle: FontHandle::default(),
x_advance_lpx: adv,
position_lpx: [0.0, 0.0],
cluster,
direction: Direction::Ltr,
}
}
fn vline(glyphs: Vec<ShapedGlyph>, byte_start: usize, byte_end: usize, y: f32) -> VisualLine {
let width: f32 = glyphs.iter().map(|g| g.x_advance_lpx).sum();
VisualLine {
line: ShapedLine {
glyphs,
width_lpx: width,
ascent_lpx: 10.0,
descent_lpx: -2.0,
y_offset_lpx: y,
base_direction: Direction::Ltr,
runs: Vec::new(),
},
byte_start,
byte_end,
}
}
fn layout_abcd() -> MultilineLayout {
MultilineLayout {
lines: vec![
vline(vec![glyph(0, 5.0), glyph(1, 6.0)], 0, 3, 0.0),
vline(vec![glyph(3, 7.0), glyph(4, 8.0)], 3, 5, 12.0),
],
total_height_lpx: 24.0,
line_height_lpx: 12.0,
}
}
fn layout_3lines() -> MultilineLayout {
MultilineLayout {
lines: vec![
vline(
vec![glyph(0, 10.0), glyph(1, 10.0), glyph(2, 10.0)],
0,
4,
0.0,
),
vline(vec![glyph(4, 10.0), glyph(5, 10.0)], 4, 7, 12.0),
vline(
vec![glyph(7, 10.0), glyph(8, 10.0), glyph(9, 10.0)],
7,
10,
24.0,
),
],
total_height_lpx: 36.0,
line_height_lpx: 12.0,
}
}
#[test]
fn byte_at_point_resolves_line_and_column() {
let text = "ab\ncd";
let l = layout_abcd();
assert_eq!(
byte_at_point(&l, text, 100.0, 50.0, 100.0 + 20.0, 50.0 + 3.0),
2
);
assert_eq!(
byte_at_point(&l, text, 100.0, 50.0, 100.0 - 5.0, 50.0 + 3.0),
0
);
let b = byte_at_point(&l, text, 100.0, 50.0, 100.0 + 1.0, 50.0 + 15.0);
assert_eq!(b, 3, "left edge of line1 → its byte_start");
}
#[test]
fn byte_at_point_clamps_y_out_of_band() {
let text = "ab\ncd";
let l = layout_abcd();
assert_eq!(
byte_at_point(&l, text, 100.0, 50.0, 100.0 + 100.0, 50.0 + 999.0),
5
);
assert_eq!(
byte_at_point(&l, text, 100.0, 50.0, 100.0 + 100.0, 50.0 - 999.0),
0
);
}
#[test]
fn byte_at_point_line_boundary_y_picks_lower_line() {
let text = "ab\ncd";
let l = layout_abcd();
assert_eq!(byte_at_point(&l, text, 0.0, 0.0, -5.0, 12.0), 3);
}
#[test]
fn selection_rects_three_line_span() {
let l = layout_3lines();
let rects = selection_rects(&l, 1, 9, 100.0);
assert_eq!(rects.len(), 3, "one rect per spanned line");
assert_eq!(
rects[0],
SelectionRect {
x_lpx: 10.0,
y_lpx: 0.0,
width_lpx: 20.0
}
);
assert_eq!(
rects[1],
SelectionRect {
x_lpx: 0.0,
y_lpx: 12.0,
width_lpx: 100.0
}
);
assert_eq!(
rects[2],
SelectionRect {
x_lpx: 0.0,
y_lpx: 24.0,
width_lpx: 20.0
}
);
}
#[test]
fn selection_rects_single_line() {
let l = MultilineLayout {
lines: vec![vline(
vec![
glyph(0, 10.0),
glyph(1, 10.0),
glyph(2, 10.0),
glyph(3, 10.0),
],
0,
4,
0.0,
)],
total_height_lpx: 12.0,
line_height_lpx: 12.0,
};
let rects = selection_rects(&l, 1, 3, 100.0);
assert_eq!(rects.len(), 1);
assert_eq!(
rects[0],
SelectionRect {
x_lpx: 10.0,
y_lpx: 0.0,
width_lpx: 20.0
}
);
}
#[test]
fn selection_rects_normalized_range_is_direction_agnostic() {
let l = layout_3lines();
let ltr = selection_rects(&l, 1, 9, 100.0);
let rtl = selection_rects(&l, 1, 9, 100.0);
assert_eq!(ltr, rtl);
}
#[test]
fn selection_rects_empty_selection_is_zero_rects() {
let l = layout_3lines();
assert!(selection_rects(&l, 4, 4, 100.0).is_empty());
assert!(selection_rects(&l, 5, 2, 100.0).is_empty());
}
#[test]
fn compose_display_inserts_at_caret() {
assert_eq!(compose_display("ab", 0, "XY"), "XYab");
assert_eq!(compose_display("ab", 1, "XY"), "aXYb");
assert_eq!(compose_display("ab", 2, "XY"), "abXY");
}
#[test]
fn compose_display_empty_preedit_is_clone() {
assert_eq!(compose_display("hello", 3, ""), "hello");
assert_eq!(compose_display("", 0, ""), "");
}
#[test]
fn compose_display_clamps_and_floors_caret() {
assert_eq!(compose_display("ab", 99, "X"), "abX");
assert_eq!(compose_display("é", 1, "X"), "Xé");
}
#[test]
fn compose_display_multibyte_preedit_and_text() {
assert_eq!(compose_display("ab", 2, "你好"), "ab你好");
assert_eq!(compose_display("ab", 0, "你好"), "你好ab");
}
#[test]
fn preedit_runs_hug_glyphs_not_content_width() {
let l = layout_3lines();
let runs = preedit_runs(&l, 1, 9);
assert_eq!(runs.len(), 3, "one run per spanned line");
assert_eq!(
runs[0],
PreeditRun {
line_idx: 0,
x_lpx: 10.0,
width_lpx: 20.0
}
);
assert_eq!(
runs[1],
PreeditRun {
line_idx: 1,
x_lpx: 0.0,
width_lpx: 20.0
}
);
assert_eq!(
runs[2],
PreeditRun {
line_idx: 2,
x_lpx: 0.0,
width_lpx: 20.0
}
);
}
#[test]
fn preedit_runs_empty_range_is_none() {
let l = layout_3lines();
assert!(preedit_runs(&l, 4, 4).is_empty());
assert!(preedit_runs(&l, 9, 2).is_empty());
}
#[test]
fn selection_rects_to_line_break_is_full_width() {
let l = layout_abcd();
let rects = selection_rects(&l, 0, 3, 200.0);
assert_eq!(rects.len(), 1);
assert_eq!(
rects[0],
SelectionRect {
x_lpx: 0.0,
y_lpx: 0.0,
width_lpx: 200.0
}
);
}
}