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