use super::bidi_runs::{run_width, within_run_caret_x};
use crate::ShapedLine;
use unicode_segmentation::UnicodeSegmentation;
pub fn byte_at_pixel_x(line: &ShapedLine, text: &str, x_lpx: f32) -> usize {
if !line.runs.is_empty() {
return run_byte_at_x(line, x_lpx);
}
if x_lpx <= 0.0 {
return 0;
}
if x_lpx >= line.width_lpx {
return text.len();
}
let mut cluster_pen: Vec<(usize, f32)> = Vec::with_capacity(line.glyphs.len());
let mut pen = 0.0f32;
for g in &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;
}
if *cursor < cluster_pen.len() {
cluster_pen[*cursor].1
} else {
line.width_lpx
}
};
let mut last_b = 0usize;
let mut last_x = 0.0f32;
for (b, _) in text.grapheme_indices(true) {
if b == 0 {
continue;
}
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 + line.width_lpx) * 0.5;
if x_lpx < mid { last_b } else { text.len() }
}
pub fn run_byte_at_x(line: &ShapedLine, x_lpx: f32) -> usize {
if line.runs.is_empty() || line.glyphs.is_empty() {
return 0;
}
let last = line.runs.len() - 1;
let mut vi = last;
let mut start_x = 0.0f32;
let mut acc = 0.0f32;
for (i, r) in line.runs.iter().enumerate() {
let w = run_width(line, &r.byte_range);
if x_lpx < acc + w || i == last {
vi = i;
start_x = acc;
break;
}
acc += w;
}
let run = &line.runs[vi];
let local = x_lpx - start_x;
let mut cands: Vec<(f32, usize)> = Vec::new();
let mut last_cluster: Option<usize> = None;
for g in line.glyphs.iter() {
let c = g.cluster as usize;
if !run.byte_range.contains(&c) || last_cluster == Some(c) {
continue;
}
last_cluster = Some(c);
cands.push((within_run_caret_x(line, run, c), c));
}
let end = run.byte_range.end;
let end_is_shared = line.runs.iter().any(|r| r.byte_range.start == end);
if !end_is_shared {
cands.push((within_run_caret_x(line, run, end), end));
}
if cands.is_empty() {
return run.byte_range.start;
}
cands.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
for w in cands.windows(2) {
let (x0, b0) = w[0];
let (x1, b1) = w[1];
if local < x1 {
let mid = (x0 + x1) * 0.5;
return if local < mid { b0 } else { b1 };
}
}
cands.last().map(|&(_, b)| b).unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::super::caret::pixel_x_at_byte;
use super::super::test_support::*;
use super::*;
use crate::ShapedLine;
use crate::types::{Direction, ShapedGlyph};
#[test]
fn midpoint_rounding() {
let l = line(vec![glyph(0, 10.0), glyph(1, 10.0)]);
assert_eq!(byte_at_pixel_x(&l, "ab", 4.9), 0);
assert_eq!(byte_at_pixel_x(&l, "ab", 5.0), 1);
assert_eq!(byte_at_pixel_x(&l, "ab", 14.9), 1);
assert_eq!(byte_at_pixel_x(&l, "ab", 15.0), 2);
}
fn byte_at_pixel_x_reference(line: &ShapedLine, text: &str, x_lpx: f32) -> usize {
if x_lpx <= 0.0 {
return 0;
}
if x_lpx >= line.width_lpx {
return text.len();
}
let mut last_b = 0usize;
let mut last_x = 0.0f32;
for (b, _) in unicode_segmentation::UnicodeSegmentation::grapheme_indices(text, true) {
if b == 0 {
continue;
}
let x = pixel_x_at_byte(line, text, b);
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 + line.width_lpx) * 0.5;
if x_lpx < mid { last_b } else { text.len() }
}
#[test]
fn large_input_agreement() {
let n = 500usize;
let text: String = "a".repeat(n);
let glyphs: Vec<ShapedGlyph> = (0..n)
.map(|i| glyph(i as u32, 3.0 + (i % 7) as f32))
.collect();
let l = line(glyphs);
let mut x = -5.0f32;
while x <= l.width_lpx + 5.0 {
assert_eq!(
byte_at_pixel_x(&l, &text, x),
byte_at_pixel_x_reference(&l, &text, x),
"mismatch at x={x}"
);
x += 0.37;
}
}
#[test]
fn rtl_hit_test_roundtrips() {
let glyphs = vec![
dglyph(6, 7.0, Direction::Rtl),
dglyph(3, 8.0, Direction::Rtl),
dglyph(0, 9.0, Direction::Rtl),
];
let l = run_line(glyphs, vec![run(0..9, Direction::Rtl)]);
let text = "日本語";
for &byte in &[0usize, 3, 6, 9] {
let x = pixel_x_at_byte(&l, text, byte);
assert_eq!(
byte_at_pixel_x(&l, text, x),
byte,
"x={x} should round-trip to byte {byte}"
);
}
assert_eq!(byte_at_pixel_x(&l, text, -100.0), 9);
assert_eq!(byte_at_pixel_x(&l, text, 1000.0), 0);
}
#[test]
fn mixed_ltr_rtl_hit_test_roundtrips_across_seam() {
let glyphs = vec![
dglyph(0, 5.0, Direction::Ltr),
dglyph(1, 6.0, Direction::Ltr),
dglyph(4, 7.0, Direction::Rtl),
dglyph(2, 8.0, Direction::Rtl),
];
let l = run_line(
glyphs,
vec![run(0..2, Direction::Ltr), run(2..6, Direction::Rtl)],
);
let text = "abאב";
for &byte in &[0usize, 1, 2, 4, 6] {
let x = pixel_x_at_byte(&l, text, byte);
assert_eq!(
byte_at_pixel_x(&l, text, x),
byte,
"x={x} should round-trip to byte {byte}"
);
}
assert_eq!(byte_at_pixel_x(&l, text, 10.0), 1);
}
}