cranpose_ui/
text_layout_result.rs1use 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)]
70pub struct TextLayoutResult {
71 pub width: f32,
73 pub height: f32,
75 pub line_height: f32,
77 glyph_x_positions: Vec<f32>,
78 cursor_positions_monotonic: bool,
79 char_to_byte: Vec<usize>,
80 pub lines: Vec<LineLayout>,
82 glyph_layouts: Vec<GlyphLayout>,
83 text_hash: u64,
84}
85
86impl TextLayoutResult {
87 pub fn new(text: &str, data: TextLayoutData) -> Self {
89 Self {
90 width: data.width,
91 height: data.height,
92 line_height: data.line_height,
93 cursor_positions_monotonic: data
94 .glyph_x_positions
95 .windows(2)
96 .all(|pair| pair[0] <= pair[1]),
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
109 .char_to_byte
110 .partition_point(|&byte| byte <= byte_offset)
111 .saturating_sub(1);
112
113 self.glyph_x_positions
114 .get(char_idx)
115 .copied()
116 .unwrap_or(self.width)
117 }
118
119 pub fn get_offset_for_x(&self, x: f32) -> usize {
122 if self.glyph_x_positions.is_empty() {
123 return 0;
124 }
125 if !self.cursor_positions_monotonic {
126 return self
127 .glyph_x_positions
128 .iter()
129 .enumerate()
130 .min_by(|(_, left), (_, right)| (**left - x).abs().total_cmp(&(**right - x).abs()))
131 .and_then(|(index, _)| self.char_to_byte.get(index))
132 .copied()
133 .unwrap_or(0);
134 }
135
136 let char_idx = match self
137 .glyph_x_positions
138 .binary_search_by(|pos| pos.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal))
139 {
140 Ok(i) => i,
141 Err(i) => {
142 if i == 0 {
143 0
144 } else if i >= self.glyph_x_positions.len() {
145 self.glyph_x_positions.len() - 1
146 } else {
147 let before = self.glyph_x_positions[i - 1];
148 let after = self.glyph_x_positions[i];
149 if (x - before) < (after - x) { i - 1 } else { i }
150 }
151 }
152 };
153
154 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);
186 char_to_byte.push(text.len());
187
188 let mut line_x = 0.0;
189 let mut line_y = 0.0;
190 let mut line_index = 0usize;
191 for (byte_offset, c) in text.char_indices() {
192 if c == '\n' {
193 line_index = line_index.saturating_add(1);
194 line_y += line_height;
195 line_x = 0.0;
196 continue;
197 }
198 let glyph_start = byte_offset;
199 let glyph_end = glyph_start + c.len_utf8();
200 glyph_layouts.push(GlyphLayout {
201 line_index,
202 start_offset: glyph_start,
203 end_offset: glyph_end,
204 x: line_x,
205 y: line_y,
206 width: char_width,
207 height: line_height,
208 });
209 line_x += char_width;
210 }
211
212 let line_texts: Vec<&str> = text.split('\n').collect();
213 let line_count = line_texts.len();
214 let mut lines = Vec::with_capacity(line_count);
215 let mut line_start = 0;
216 let mut y = 0.0;
217 let mut max_width: f32 = 0.0;
218
219 for (i, line_text) in line_texts.iter().enumerate() {
220 let line_end = if i == line_count - 1 {
221 text.len()
222 } else {
223 line_start + line_text.len()
224 };
225
226 let line_width = line_text.chars().count() as f32 * char_width;
227 max_width = max_width.max(line_width);
228
229 lines.push(LineLayout {
230 start_offset: line_start,
231 end_offset: line_end,
232 y,
233 height: line_height,
234 });
235
236 line_start = line_end + 1;
237 y += line_height;
238 }
239
240 if lines.is_empty() {
241 lines.push(LineLayout {
242 start_offset: 0,
243 end_offset: 0,
244 y: 0.0,
245 height: line_height,
246 });
247 }
248
249 Self::new(
250 text,
251 TextLayoutData {
252 width: max_width,
253 height: lines.len() as f32 * line_height,
254 line_height,
255 glyph_x_positions,
256 char_to_byte,
257 lines,
258 glyph_layouts,
259 },
260 )
261 }
262}
263
264#[cfg(test)]
265#[path = "tests/text_layout_result_tests.rs"]
266mod tests;