1use cranpose_core::hash::default;
8use std::hash::{Hash, Hasher};
9
10#[derive(Debug, Clone)]
12pub struct LineLayout {
13 pub start_offset: usize,
15 pub end_offset: usize,
17 pub y: f32,
19 pub height: f32,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct GlyphLayout {
26 pub line_index: usize,
28 pub start_offset: usize,
30 pub end_offset: usize,
32 pub x: f32,
34 pub y: f32,
36 pub width: f32,
38 pub height: f32,
40}
41
42#[derive(Debug, Clone)]
49pub struct TextLayoutData {
50 pub width: f32,
52 pub height: f32,
54 pub line_height: f32,
56 pub glyph_x_positions: Vec<f32>,
58 pub char_to_byte: Vec<usize>,
60 pub lines: Vec<LineLayout>,
62 pub glyph_layouts: Vec<GlyphLayout>,
64}
65
66#[derive(Debug, Clone)]
67pub struct TextLayoutResult {
68 pub width: f32,
70 pub height: f32,
72 pub line_height: f32,
74 glyph_x_positions: Vec<f32>,
78 char_to_byte: Vec<usize>,
81 pub lines: Vec<LineLayout>,
83 glyph_layouts: Vec<GlyphLayout>,
85 text_hash: u64,
87}
88
89impl TextLayoutResult {
90 pub fn new(text: &str, data: TextLayoutData) -> Self {
92 Self {
93 width: data.width,
94 height: data.height,
95 line_height: data.line_height,
96 glyph_x_positions: data.glyph_x_positions,
97 char_to_byte: data.char_to_byte,
98 lines: data.lines,
99 glyph_layouts: data.glyph_layouts,
100 text_hash: Self::hash_text(text),
101 }
102 }
103
104 pub fn get_cursor_x(&self, byte_offset: usize) -> f32 {
107 let char_idx = self
109 .char_to_byte
110 .iter()
111 .position(|&b| b > byte_offset)
112 .map(|i| i.saturating_sub(1))
113 .unwrap_or(self.char_to_byte.len().saturating_sub(1));
114
115 self.glyph_x_positions
117 .get(char_idx)
118 .copied()
119 .unwrap_or(self.width)
120 }
121
122 pub fn get_offset_for_x(&self, x: f32) -> usize {
125 if self.glyph_x_positions.is_empty() {
126 return 0;
127 }
128
129 let char_idx = match self
131 .glyph_x_positions
132 .binary_search_by(|pos| pos.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal))
133 {
134 Ok(i) => i,
135 Err(i) => {
136 if i == 0 {
138 0
139 } else if i >= self.glyph_x_positions.len() {
140 self.glyph_x_positions.len() - 1
141 } else {
142 let before = self.glyph_x_positions[i - 1];
143 let after = self.glyph_x_positions[i];
144 if (x - before) < (after - x) {
145 i - 1
146 } else {
147 i
148 }
149 }
150 }
151 };
152
153 self.char_to_byte.get(char_idx).copied().unwrap_or(0)
155 }
156
157 pub fn is_valid_for(&self, text: &str) -> bool {
159 self.text_hash == Self::hash_text(text)
160 }
161
162 pub fn glyph_layouts(&self) -> &[GlyphLayout] {
164 &self.glyph_layouts
165 }
166
167 fn hash_text(text: &str) -> u64 {
168 let mut hasher = default::new();
169 text.hash(&mut hasher);
170 hasher.finish()
171 }
172
173 pub fn monospaced(text: &str, char_width: f32, line_height: f32) -> Self {
175 let mut glyph_x_positions = Vec::new();
176 let mut char_to_byte = Vec::new();
177 let mut glyph_layouts = Vec::new();
178 let mut cursor_x = 0.0;
179
180 for (byte_offset, _c) in text.char_indices() {
181 glyph_x_positions.push(cursor_x);
182 char_to_byte.push(byte_offset);
183 cursor_x += char_width;
184 }
185 glyph_x_positions.push(cursor_x);
187 char_to_byte.push(text.len());
188
189 let mut line_x = 0.0;
190 let mut line_y = 0.0;
191 let mut line_index = 0usize;
192 for (byte_offset, c) in text.char_indices() {
193 if c == '\n' {
194 line_index = line_index.saturating_add(1);
195 line_y += line_height;
196 line_x = 0.0;
197 continue;
198 }
199 let glyph_start = byte_offset;
200 let glyph_end = glyph_start + c.len_utf8();
201 glyph_layouts.push(GlyphLayout {
202 line_index,
203 start_offset: glyph_start,
204 end_offset: glyph_end,
205 x: line_x,
206 y: line_y,
207 width: char_width,
208 height: line_height,
209 });
210 line_x += char_width;
211 }
212
213 let line_texts: Vec<&str> = text.split('\n').collect();
215 let line_count = line_texts.len();
216 let mut lines = Vec::with_capacity(line_count);
217 let mut line_start = 0;
218 let mut y = 0.0;
219 let mut max_width: f32 = 0.0;
220
221 for (i, line_text) in line_texts.iter().enumerate() {
222 let line_end = if i == line_count - 1 {
223 text.len()
224 } else {
225 line_start + line_text.len()
226 };
227
228 let line_width = line_text.chars().count() as f32 * char_width;
230 max_width = max_width.max(line_width);
231
232 lines.push(LineLayout {
233 start_offset: line_start,
234 end_offset: line_end,
235 y,
236 height: line_height,
237 });
238
239 line_start = line_end + 1; y += line_height;
241 }
242
243 if lines.is_empty() {
245 lines.push(LineLayout {
246 start_offset: 0,
247 end_offset: 0,
248 y: 0.0,
249 height: line_height,
250 });
251 }
252
253 Self::new(
254 text,
255 TextLayoutData {
256 width: max_width,
257 height: lines.len() as f32 * line_height,
258 line_height,
259 glyph_x_positions,
260 char_to_byte,
261 lines,
262 glyph_layouts,
263 },
264 )
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_monospaced_layout() {
274 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
275
276 assert_eq!(layout.get_cursor_x(0), 0.0); assert_eq!(layout.get_cursor_x(5), 50.0); }
280
281 #[test]
282 fn test_get_offset_for_x() {
283 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
284
285 let offset = layout.get_offset_for_x(25.0);
287 assert!(offset == 2 || offset == 3);
288 }
289
290 #[test]
291 fn test_multiline() {
292 let layout = TextLayoutResult::monospaced("Hi\nWorld", 10.0, 20.0);
293
294 assert_eq!(layout.lines.len(), 2);
295 assert_eq!(layout.lines[0].start_offset, 0);
296 assert_eq!(layout.lines[1].start_offset, 3); }
298
299 #[test]
300 fn test_validity() {
301 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
302
303 assert!(layout.is_valid_for("Hello"));
304 assert!(!layout.is_valid_for("World"));
305 }
306}