cranpose_ui/
text_layout_result.rs1use std::collections::hash_map::DefaultHasher;
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)]
30pub struct TextLayoutResult {
31 pub width: f32,
33 pub height: f32,
35 pub line_height: f32,
37 glyph_x_positions: Vec<f32>,
41 char_to_byte: Vec<usize>,
44 pub lines: Vec<LineLayout>,
46 text_hash: u64,
48}
49
50impl TextLayoutResult {
51 pub fn new(
53 width: f32,
54 height: f32,
55 line_height: f32,
56 glyph_x_positions: Vec<f32>,
57 char_to_byte: Vec<usize>,
58 lines: Vec<LineLayout>,
59 text: &str,
60 ) -> Self {
61 Self {
62 width,
63 height,
64 line_height,
65 glyph_x_positions,
66 char_to_byte,
67 lines,
68 text_hash: Self::hash_text(text),
69 }
70 }
71
72 pub fn get_cursor_x(&self, byte_offset: usize) -> f32 {
75 let char_idx = self
77 .char_to_byte
78 .iter()
79 .position(|&b| b > byte_offset)
80 .map(|i| i.saturating_sub(1))
81 .unwrap_or(self.char_to_byte.len().saturating_sub(1));
82
83 self.glyph_x_positions
85 .get(char_idx)
86 .copied()
87 .unwrap_or(self.width)
88 }
89
90 pub fn get_offset_for_x(&self, x: f32) -> usize {
93 if self.glyph_x_positions.is_empty() {
94 return 0;
95 }
96
97 let char_idx = match self
99 .glyph_x_positions
100 .binary_search_by(|pos| pos.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal))
101 {
102 Ok(i) => i,
103 Err(i) => {
104 if i == 0 {
106 0
107 } else if i >= self.glyph_x_positions.len() {
108 self.glyph_x_positions.len() - 1
109 } else {
110 let before = self.glyph_x_positions[i - 1];
111 let after = self.glyph_x_positions[i];
112 if (x - before) < (after - x) {
113 i - 1
114 } else {
115 i
116 }
117 }
118 }
119 };
120
121 self.char_to_byte.get(char_idx).copied().unwrap_or(0)
123 }
124
125 pub fn is_valid_for(&self, text: &str) -> bool {
127 self.text_hash == Self::hash_text(text)
128 }
129
130 fn hash_text(text: &str) -> u64 {
131 let mut hasher = DefaultHasher::new();
132 text.hash(&mut hasher);
133 hasher.finish()
134 }
135
136 pub fn monospaced(text: &str, char_width: f32, line_height: f32) -> Self {
138 let mut glyph_x_positions = Vec::new();
139 let mut char_to_byte = Vec::new();
140 let mut x = 0.0;
141
142 for (byte_offset, _c) in text.char_indices() {
143 glyph_x_positions.push(x);
144 char_to_byte.push(byte_offset);
145 x += char_width;
146 }
147 glyph_x_positions.push(x);
149 char_to_byte.push(text.len());
150
151 let line_texts: Vec<&str> = text.split('\n').collect();
153 let line_count = line_texts.len();
154 let mut lines = Vec::with_capacity(line_count);
155 let mut line_start = 0;
156 let mut y = 0.0;
157 let mut max_width: f32 = 0.0;
158
159 for (i, line_text) in line_texts.iter().enumerate() {
160 let line_end = if i == line_count - 1 {
161 text.len()
162 } else {
163 line_start + line_text.len()
164 };
165
166 let line_width = line_text.chars().count() as f32 * char_width;
168 max_width = max_width.max(line_width);
169
170 lines.push(LineLayout {
171 start_offset: line_start,
172 end_offset: line_end,
173 y,
174 height: line_height,
175 });
176
177 line_start = line_end + 1; y += line_height;
179 }
180
181 if lines.is_empty() {
183 lines.push(LineLayout {
184 start_offset: 0,
185 end_offset: 0,
186 y: 0.0,
187 height: line_height,
188 });
189 }
190
191 Self::new(
192 max_width,
193 lines.len() as f32 * line_height,
194 line_height,
195 glyph_x_positions,
196 char_to_byte,
197 lines,
198 text,
199 )
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_monospaced_layout() {
209 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
210
211 assert_eq!(layout.get_cursor_x(0), 0.0); assert_eq!(layout.get_cursor_x(5), 50.0); }
215
216 #[test]
217 fn test_get_offset_for_x() {
218 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
219
220 let offset = layout.get_offset_for_x(25.0);
222 assert!(offset == 2 || offset == 3);
223 }
224
225 #[test]
226 fn test_multiline() {
227 let layout = TextLayoutResult::monospaced("Hi\nWorld", 10.0, 20.0);
228
229 assert_eq!(layout.lines.len(), 2);
230 assert_eq!(layout.lines[0].start_offset, 0);
231 assert_eq!(layout.lines[1].start_offset, 3); }
233
234 #[test]
235 fn test_validity() {
236 let layout = TextLayoutResult::monospaced("Hello", 10.0, 20.0);
237
238 assert!(layout.is_valid_for("Hello"));
239 assert!(!layout.is_valid_for("World"));
240 }
241}