use unicode_segmentation::UnicodeSegmentation;
use super::MultilineLayout;
impl MultilineLayout {
pub fn line_caret_end(&self, text: &str, idx: usize) -> usize {
match self.lines.get(idx) {
Some(vline) => {
if !vline.line.runs.is_empty() {
return vline
.line
.runs
.iter()
.map(|r| r.byte_range.end)
.max()
.unwrap_or(vline.byte_start);
}
match vline.line.glyphs.last() {
Some(g) => next_grapheme(text, g.cluster as usize),
None => vline.byte_start,
}
}
None => 0,
}
}
pub fn byte_at_line_x(&self, text: &str, line_idx: usize, x_lpx: f32) -> usize {
let Some(vline) = self.lines.get(line_idx) else {
return 0;
};
let start = vline.byte_start;
let end = self.line_caret_end(text, line_idx);
let width = vline.line.width_lpx;
if !vline.line.runs.is_empty() {
return crate::glyph_geometry::run_byte_at_x(&vline.line, x_lpx).clamp(start, end);
}
if x_lpx <= 0.0 {
return start;
}
if x_lpx >= width {
return end;
}
let mut cluster_pen: Vec<(usize, f32)> = Vec::with_capacity(vline.line.glyphs.len());
let mut pen = 0.0f32;
for g in &vline.line.glyphs {
let c = g.cluster as usize;
match cluster_pen.last() {
Some(&(last_c, _)) if last_c == c => {}
_ => cluster_pen.push((c, pen)),
}
pen += g.x_advance_lpx;
}
let mut cursor = 0usize;
let pen_at = |byte: usize, cursor: &mut usize| -> f32 {
while *cursor < cluster_pen.len() && cluster_pen[*cursor].0 < byte {
*cursor += 1;
}
cluster_pen.get(*cursor).map(|&(_, p)| p).unwrap_or(width)
};
let mut last_b = start;
let mut last_x = 0.0f32;
for (rel, _) in text[start..end].grapheme_indices(true) {
if rel == 0 {
continue;
}
let b = start + rel;
let x = pen_at(b, &mut cursor);
if x_lpx < x {
let mid = (last_x + x) * 0.5;
return if x_lpx < mid { last_b } else { b };
}
last_b = b;
last_x = x;
}
let mid = (last_x + width) * 0.5;
if x_lpx < mid { last_b } else { end }
}
}
fn next_grapheme(text: &str, byte: usize) -> usize {
if byte >= text.len() {
return text.len();
}
let mut cursor = unicode_segmentation::GraphemeCursor::new(byte, text.len(), true);
match cursor.next_boundary(text, 0) {
Ok(Some(b)) => b,
_ => text.len(),
}
}
#[cfg(test)]
mod tests {
use super::super::MultilineLayout;
use super::super::test_helpers::*;
#[test]
fn line_caret_end_excludes_trailing_newline() {
let text = "ab\ncd";
let l = two_line_layout();
assert_eq!(l.line_caret_end(text, 0), 2);
assert_eq!(l.line_caret_end(text, 1), 5);
}
#[test]
fn line_caret_end_empty_line_is_byte_start() {
let text = "a\n\nb"; let l = MultilineLayout {
lines: vec![
vline(vec![glyph(0, 5.0)], 0, 2, 0.0),
vline(vec![], 2, 3, 12.0),
vline(vec![glyph(3, 6.0)], 3, 4, 24.0),
],
total_height_lpx: 36.0,
line_height_lpx: 12.0,
};
assert_eq!(l.line_caret_end(text, 1), 2);
}
#[test]
fn byte_at_line_x_clamps_to_line_range() {
let text = "ab\ncd";
let l = two_line_layout();
assert_eq!(l.byte_at_line_x(text, 0, -10.0), 0);
assert_eq!(l.byte_at_line_x(text, 1, 0.0), 3);
assert_eq!(l.byte_at_line_x(text, 0, 1000.0), 2);
assert_eq!(l.byte_at_line_x(text, 1, 1000.0), 5);
}
#[test]
fn byte_at_line_x_roundtrips_with_caret_position() {
let text = "ab\ncd";
let l = two_line_layout();
for &(line_idx, byte) in &[(0usize, 0usize), (0, 1), (0, 2), (1, 3), (1, 4), (1, 5)] {
let (_, x, _) = l.caret_position(byte);
assert_eq!(
l.byte_at_line_x(text, line_idx, x),
byte,
"x={x} on line {line_idx} should map back to byte {byte}"
);
}
}
#[test]
fn byte_at_line_x_midpoint_rule() {
let text = "ab\ncd";
let l = two_line_layout();
assert_eq!(l.byte_at_line_x(text, 0, 2.4), 0);
assert_eq!(l.byte_at_line_x(text, 0, 2.6), 1);
}
}