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>,
76 char_to_byte: Vec<usize>,
77 pub lines: Vec<LineLayout>,
79 glyph_layouts: Vec<GlyphLayout>,
80 text_hash: u64,
81}
82
83impl TextLayoutResult {
84 pub fn new(text: &str, data: TextLayoutData) -> Self {
86 Self {
87 width: data.width,
88 height: data.height,
89 line_height: data.line_height,
90 glyph_x_positions: data.glyph_x_positions,
91 char_to_byte: data.char_to_byte,
92 lines: data.lines,
93 glyph_layouts: data.glyph_layouts,
94 text_hash: Self::hash_text(text),
95 }
96 }
97
98 pub fn get_cursor_x(&self, byte_offset: usize) -> f32 {
101 let char_idx = self
102 .char_to_byte
103 .iter()
104 .position(|&b| b > byte_offset)
105 .map_or_else(
106 || self.char_to_byte.len().saturating_sub(1),
107 |i| i.saturating_sub(1),
108 );
109
110 self.glyph_x_positions
111 .get(char_idx)
112 .copied()
113 .unwrap_or(self.width)
114 }
115
116 pub fn get_offset_for_x(&self, x: f32) -> usize {
119 if self.glyph_x_positions.is_empty() {
120 return 0;
121 }
122
123 let char_idx = match self
124 .glyph_x_positions
125 .binary_search_by(|pos| pos.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal))
126 {
127 Ok(i) => i,
128 Err(i) => {
129 if i == 0 {
130 0
131 } else if i >= self.glyph_x_positions.len() {
132 self.glyph_x_positions.len() - 1
133 } else {
134 let before = self.glyph_x_positions[i - 1];
135 let after = self.glyph_x_positions[i];
136 if (x - before) < (after - x) { i - 1 } else { i }
137 }
138 }
139 };
140
141 self.char_to_byte.get(char_idx).copied().unwrap_or(0)
142 }
143
144 pub fn is_valid_for(&self, text: &str) -> bool {
146 self.text_hash == Self::hash_text(text)
147 }
148
149 pub fn glyph_layouts(&self) -> &[GlyphLayout] {
151 &self.glyph_layouts
152 }
153
154 fn hash_text(text: &str) -> u64 {
155 let mut hasher = default::new();
156 text.hash(&mut hasher);
157 hasher.finish()
158 }
159
160 pub fn monospaced(text: &str, char_width: f32, line_height: f32) -> Self {
162 let mut glyph_x_positions = Vec::new();
163 let mut char_to_byte = Vec::new();
164 let mut glyph_layouts = Vec::new();
165 let mut cursor_x = 0.0;
166
167 for (byte_offset, _c) in text.char_indices() {
168 glyph_x_positions.push(cursor_x);
169 char_to_byte.push(byte_offset);
170 cursor_x += char_width;
171 }
172 glyph_x_positions.push(cursor_x);
173 char_to_byte.push(text.len());
174
175 let mut line_x = 0.0;
176 let mut line_y = 0.0;
177 let mut line_index = 0usize;
178 for (byte_offset, c) in text.char_indices() {
179 if c == '\n' {
180 line_index = line_index.saturating_add(1);
181 line_y += line_height;
182 line_x = 0.0;
183 continue;
184 }
185 let glyph_start = byte_offset;
186 let glyph_end = glyph_start + c.len_utf8();
187 glyph_layouts.push(GlyphLayout {
188 line_index,
189 start_offset: glyph_start,
190 end_offset: glyph_end,
191 x: line_x,
192 y: line_y,
193 width: char_width,
194 height: line_height,
195 });
196 line_x += char_width;
197 }
198
199 let line_texts: Vec<&str> = text.split('\n').collect();
200 let line_count = line_texts.len();
201 let mut lines = Vec::with_capacity(line_count);
202 let mut line_start = 0;
203 let mut y = 0.0;
204 let mut max_width: f32 = 0.0;
205
206 for (i, line_text) in line_texts.iter().enumerate() {
207 let line_end = if i == line_count - 1 {
208 text.len()
209 } else {
210 line_start + line_text.len()
211 };
212
213 let line_width = line_text.chars().count() as f32 * char_width;
214 max_width = max_width.max(line_width);
215
216 lines.push(LineLayout {
217 start_offset: line_start,
218 end_offset: line_end,
219 y,
220 height: line_height,
221 });
222
223 line_start = line_end + 1;
224 y += line_height;
225 }
226
227 if lines.is_empty() {
228 lines.push(LineLayout {
229 start_offset: 0,
230 end_offset: 0,
231 y: 0.0,
232 height: line_height,
233 });
234 }
235
236 Self::new(
237 text,
238 TextLayoutData {
239 width: max_width,
240 height: lines.len() as f32 * line_height,
241 line_height,
242 glyph_x_positions,
243 char_to_byte,
244 lines,
245 glyph_layouts,
246 },
247 )
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_monospaced_layout() {
257 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
258
259 assert_eq!(layout.get_cursor_x(0), 0.0);
260 assert_eq!(layout.get_cursor_x(5), 50.0);
261 }
262
263 #[test]
264 fn test_get_offset_for_x() {
265 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
266
267 let offset = layout.get_offset_for_x(25.0);
268 assert!(offset == 2 || offset == 3);
269 }
270
271 #[test]
272 fn test_multiline() {
273 let layout = TextLayoutResult::monospaced("Hi\nWorld", 10.0, 20.0);
274
275 assert_eq!(layout.lines.len(), 2);
276 assert_eq!(layout.lines[0].start_offset, 0);
277 assert_eq!(layout.lines[1].start_offset, 3);
278 }
279
280 #[test]
281 fn test_validity() {
282 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
283
284 assert!(layout.is_valid_for("Hello"));
285 assert!(!layout.is_valid_for("World"));
286 }
287}