1use std::sync::{OnceLock, RwLock};
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: Send + Sync + '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
105fn global_text_measurer() -> &'static RwLock<Box<dyn TextMeasurer>> {
106 static TEXT_MEASURER: OnceLock<RwLock<Box<dyn TextMeasurer>>> = OnceLock::new();
107 TEXT_MEASURER.get_or_init(|| RwLock::new(Box::new(MonospacedTextMeasurer)))
108}
109
110pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
111 let mut guard = global_text_measurer()
112 .write()
113 .expect("text measurer lock poisoned");
114 *guard = Box::new(measurer);
115}
116
117pub fn measure_text(text: &str) -> TextMetrics {
118 global_text_measurer()
119 .read()
120 .expect("text measurer lock poisoned")
121 .measure(text)
122}
123
124pub fn get_offset_for_position(text: &str, x: f32, y: f32) -> usize {
127 global_text_measurer()
128 .read()
129 .expect("text measurer lock poisoned")
130 .get_offset_for_position(text, x, y)
131}
132
133pub fn get_cursor_x_for_offset(text: &str, offset: usize) -> f32 {
136 global_text_measurer()
137 .read()
138 .expect("text measurer lock poisoned")
139 .get_cursor_x_for_offset(text, offset)
140}
141
142pub fn layout_text(text: &str) -> TextLayoutResult {
145 global_text_measurer()
146 .read()
147 .expect("text measurer lock poisoned")
148 .layout(text)
149}