1use std::hash::{Hash, Hasher};
8
9use cranpose_core::hash::default;
10
11#[derive(Debug, Clone)]
13pub struct LineLayout {
14 pub start_offset: usize,
16 pub end_offset: usize,
18 pub y: f32,
20 pub height: f32,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct GlyphLayout {
27 pub line_index: usize,
29 pub start_offset: usize,
31 pub end_offset: usize,
33 pub x: f32,
35 pub y: f32,
37 pub width: f32,
39 pub height: f32,
41}
42
43#[derive(Debug, Clone)]
50pub struct TextLayoutData {
51 pub width: f32,
53 pub height: f32,
55 pub line_height: f32,
57 pub glyph_x_positions: Vec<f32>,
59 pub char_to_byte: Vec<usize>,
61 pub lines: Vec<LineLayout>,
63 pub glyph_layouts: Vec<GlyphLayout>,
65}
66
67#[derive(Debug, Clone)]
68pub struct TextLayoutResult {
69 pub width: f32,
71 pub height: f32,
73 pub line_height: f32,
75 glyph_x_positions: Vec<f32>,
79 char_to_byte: Vec<usize>,
82 pub lines: Vec<LineLayout>,
84 glyph_layouts: Vec<GlyphLayout>,
86 text_hash: u64,
88}
89
90impl TextLayoutResult {
91 pub fn new(text: &str, data: TextLayoutData) -> Self {
93 Self {
94 width: data.width,
95 height: data.height,
96 line_height: data.line_height,
97 glyph_x_positions: data.glyph_x_positions,
98 char_to_byte: data.char_to_byte,
99 lines: data.lines,
100 glyph_layouts: data.glyph_layouts,
101 text_hash: Self::hash_text(text),
102 }
103 }
104
105 pub fn get_cursor_x(&self, byte_offset: usize) -> f32 {
108 let char_idx = self
110 .char_to_byte
111 .iter()
112 .position(|&b| b > byte_offset)
113 .map(|i| i.saturating_sub(1))
114 .unwrap_or(self.char_to_byte.len().saturating_sub(1));
115
116 self.glyph_x_positions
118 .get(char_idx)
119 .copied()
120 .unwrap_or(self.width)
121 }
122
123 pub fn get_offset_for_x(&self, x: f32) -> usize {
126 if self.glyph_x_positions.is_empty() {
127 return 0;
128 }
129
130 let char_idx = match self
132 .glyph_x_positions
133 .binary_search_by(|pos| pos.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal))
134 {
135 Ok(i) => i,
136 Err(i) => {
137 if i == 0 {
139 0
140 } else if i >= self.glyph_x_positions.len() {
141 self.glyph_x_positions.len() - 1
142 } else {
143 let before = self.glyph_x_positions[i - 1];
144 let after = self.glyph_x_positions[i];
145 if (x - before) < (after - x) {
146 i - 1
147 } else {
148 i
149 }
150 }
151 }
152 };
153
154 self.char_to_byte.get(char_idx).copied().unwrap_or(0)
156 }
157
158 pub fn is_valid_for(&self, text: &str) -> bool {
160 self.text_hash == Self::hash_text(text)
161 }
162
163 pub fn glyph_layouts(&self) -> &[GlyphLayout] {
165 &self.glyph_layouts
166 }
167
168 fn hash_text(text: &str) -> u64 {
169 let mut hasher = default::new();
170 text.hash(&mut hasher);
171 hasher.finish()
172 }
173
174 pub fn monospaced(text: &str, char_width: f32, line_height: f32) -> Self {
176 let mut glyph_x_positions = Vec::new();
177 let mut char_to_byte = Vec::new();
178 let mut glyph_layouts = Vec::new();
179 let mut cursor_x = 0.0;
180
181 for (byte_offset, _c) in text.char_indices() {
182 glyph_x_positions.push(cursor_x);
183 char_to_byte.push(byte_offset);
184 cursor_x += char_width;
185 }
186 glyph_x_positions.push(cursor_x);
188 char_to_byte.push(text.len());
189
190 let mut line_x = 0.0;
191 let mut line_y = 0.0;
192 let mut line_index = 0usize;
193 for (byte_offset, c) in text.char_indices() {
194 if c == '\n' {
195 line_index = line_index.saturating_add(1);
196 line_y += line_height;
197 line_x = 0.0;
198 continue;
199 }
200 let glyph_start = byte_offset;
201 let glyph_end = glyph_start + c.len_utf8();
202 glyph_layouts.push(GlyphLayout {
203 line_index,
204 start_offset: glyph_start,
205 end_offset: glyph_end,
206 x: line_x,
207 y: line_y,
208 width: char_width,
209 height: line_height,
210 });
211 line_x += char_width;
212 }
213
214 let line_texts: Vec<&str> = text.split('\n').collect();
216 let line_count = line_texts.len();
217 let mut lines = Vec::with_capacity(line_count);
218 let mut line_start = 0;
219 let mut y = 0.0;
220 let mut max_width: f32 = 0.0;
221
222 for (i, line_text) in line_texts.iter().enumerate() {
223 let line_end = if i == line_count - 1 {
224 text.len()
225 } else {
226 line_start + line_text.len()
227 };
228
229 let line_width = line_text.chars().count() as f32 * char_width;
231 max_width = max_width.max(line_width);
232
233 lines.push(LineLayout {
234 start_offset: line_start,
235 end_offset: line_end,
236 y,
237 height: line_height,
238 });
239
240 line_start = line_end + 1; y += line_height;
242 }
243
244 if lines.is_empty() {
246 lines.push(LineLayout {
247 start_offset: 0,
248 end_offset: 0,
249 y: 0.0,
250 height: line_height,
251 });
252 }
253
254 Self::new(
255 text,
256 TextLayoutData {
257 width: max_width,
258 height: lines.len() as f32 * line_height,
259 line_height,
260 glyph_x_positions,
261 char_to_byte,
262 lines,
263 glyph_layouts,
264 },
265 )
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_monospaced_layout() {
275 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
276
277 assert_eq!(layout.get_cursor_x(0), 0.0); assert_eq!(layout.get_cursor_x(5), 50.0); }
281
282 #[test]
283 fn test_get_offset_for_x() {
284 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
285
286 let offset = layout.get_offset_for_x(25.0);
288 assert!(offset == 2 || offset == 3);
289 }
290
291 #[test]
292 fn test_multiline() {
293 let layout = TextLayoutResult::monospaced("Hi\nWorld", 10.0, 20.0);
294
295 assert_eq!(layout.lines.len(), 2);
296 assert_eq!(layout.lines[0].start_offset, 0);
297 assert_eq!(layout.lines[1].start_offset, 3); }
299
300 #[test]
301 fn test_validity() {
302 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
303
304 assert!(layout.is_valid_for("Hello"));
305 assert!(!layout.is_valid_for("World"));
306 }
307}