1use std::cell::RefCell;
2
3use crate::text_layout_result::TextLayoutResult;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct TextMetrics {
7 pub width: f32,
8 pub height: f32,
9 pub line_height: f32,
11 pub line_count: usize,
13}
14
15pub trait TextMeasurer: 'static {
16 fn measure(&self, text: &str) -> TextMetrics;
17
18 fn get_offset_for_position(&self, text: &str, x: f32, y: f32) -> usize;
23
24 fn get_cursor_x_for_offset(&self, text: &str, offset: usize) -> f32;
27
28 fn layout(&self, text: &str) -> TextLayoutResult;
31}
32
33#[derive(Default)]
34struct MonospacedTextMeasurer;
35
36impl MonospacedTextMeasurer {
37 const CHAR_WIDTH: f32 = 8.0;
38 const LINE_HEIGHT: f32 = 20.0;
39}
40
41impl TextMeasurer for MonospacedTextMeasurer {
42 fn measure(&self, text: &str) -> TextMetrics {
43 let lines: Vec<&str> = text.split('\n').collect();
45 let line_count = lines.len().max(1);
46
47 let width = lines
49 .iter()
50 .map(|line| line.chars().count() as f32 * Self::CHAR_WIDTH)
51 .fold(0.0_f32, f32::max);
52
53 TextMetrics {
54 width,
55 height: line_count as f32 * Self::LINE_HEIGHT,
56 line_height: Self::LINE_HEIGHT,
57 line_count,
58 }
59 }
60
61 fn get_offset_for_position(&self, text: &str, x: f32, y: f32) -> usize {
62 if text.is_empty() {
63 return 0;
64 }
65
66 let line_index = (y / Self::LINE_HEIGHT).floor().max(0.0) as usize;
68 let lines: Vec<&str> = text.split('\n').collect();
69 let target_line = line_index.min(lines.len().saturating_sub(1));
70
71 let mut line_start_byte = 0;
73 for line in lines.iter().take(target_line) {
74 line_start_byte += line.len() + 1; }
76
77 let line_text = lines.get(target_line).unwrap_or(&"");
79 let char_index = (x / Self::CHAR_WIDTH).round() as usize;
80 let line_char_count = line_text.chars().count();
81 let clamped_index = char_index.min(line_char_count);
82
83 let offset_in_line = line_text
85 .char_indices()
86 .nth(clamped_index)
87 .map(|(i, _)| i)
88 .unwrap_or(line_text.len());
89
90 line_start_byte + offset_in_line
91 }
92
93 fn get_cursor_x_for_offset(&self, text: &str, offset: usize) -> f32 {
94 let clamped_offset = offset.min(text.len());
96 let char_count = text[..clamped_offset].chars().count();
97 char_count as f32 * Self::CHAR_WIDTH
98 }
99
100 fn layout(&self, text: &str) -> TextLayoutResult {
101 TextLayoutResult::monospaced(text, Self::CHAR_WIDTH, Self::LINE_HEIGHT)
102 }
103}
104
105thread_local! {
106 static TEXT_MEASURER: RefCell<Box<dyn TextMeasurer>> = RefCell::new(Box::new(MonospacedTextMeasurer));
107}
108
109pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
110 TEXT_MEASURER.with(|m| {
111 *m.borrow_mut() = Box::new(measurer);
112 });
113}
114
115pub fn measure_text(text: &str) -> TextMetrics {
116 TEXT_MEASURER.with(|m| m.borrow().measure(text))
117}
118
119pub fn get_offset_for_position(text: &str, x: f32, y: f32) -> usize {
122 TEXT_MEASURER.with(|m| m.borrow().get_offset_for_position(text, x, y))
123}
124
125pub fn get_cursor_x_for_offset(text: &str, offset: usize) -> f32 {
128 TEXT_MEASURER.with(|m| m.borrow().get_cursor_x_for_offset(text, offset))
129}
130
131pub fn layout_text(text: &str) -> TextLayoutResult {
134 TEXT_MEASURER.with(|m| m.borrow().layout(text))
135}