1 | pub use crate::unicode::CharacterData; |
2 | |
3 | use crate::unicode::{read_utf8, LinebreakData, Linebreaker, LINEBREAK_NONE}; |
4 | use crate::Font; |
5 | use crate::{ |
6 | platform::{ceil, floor}, |
7 | Metrics, |
8 | }; |
9 | use alloc::vec::*; |
10 | use core::borrow::Borrow; |
11 | use core::hash::{Hash, Hasher}; |
12 | |
13 | /// Horizontal alignment options for text when a max_width is provided. |
14 | #[derive (Copy, Clone, PartialEq)] |
15 | pub enum HorizontalAlign { |
16 | /// Aligns text to the left of the region defined by the max_width. |
17 | Left, |
18 | /// Aligns text to the center of the region defined by the max_width. |
19 | Center, |
20 | /// Aligns text to the right of the region defined by the max_width. |
21 | Right, |
22 | } |
23 | |
24 | /// Vertical alignment options for text when a max_height is provided. |
25 | #[derive (Copy, Clone, PartialEq)] |
26 | pub enum VerticalAlign { |
27 | /// Aligns text to the top of the region defined by the max_height. |
28 | Top, |
29 | /// Aligns text to the middle of the region defined by the max_height. |
30 | Middle, |
31 | /// Aligns text to the bottom of the region defined by the max_height. |
32 | Bottom, |
33 | } |
34 | |
35 | /// Wrap style is a hint for how strings of text should be wrapped to the next line. Line wrapping |
36 | /// can happen when the max width/height is reached. |
37 | #[derive (Copy, Clone, PartialEq)] |
38 | pub enum WrapStyle { |
39 | /// Word will break lines by the Unicode line breaking algorithm (Standard Annex #14) This will |
40 | /// generally break lines where you expect them to be broken at and will preserve words. |
41 | Word, |
42 | /// Letter will not preserve words, breaking into a new line after the nearest letter. |
43 | Letter, |
44 | } |
45 | |
46 | /// The direction that the Y coordinate increases in. Layout needs to be aware of your coordinate |
47 | /// system to place the glyphs correctly. |
48 | #[derive (Copy, Clone, PartialEq)] |
49 | pub enum CoordinateSystem { |
50 | /// The Y coordinate increases up relative to the window or image. The higher up on the window, |
51 | /// the more positive Y becomes. |
52 | PositiveYUp, |
53 | /// The Y coordinate increases down relative to the window or image. The lower down on the |
54 | /// window, the more positive Y becomes. |
55 | PositiveYDown, |
56 | } |
57 | |
58 | /// Settings to configure how text layout is constrained. Text layout is considered best effort and |
59 | /// layout may violate the constraints defined here if they prevent text from being laid out. |
60 | #[derive (Copy, Clone, PartialEq)] |
61 | pub struct LayoutSettings { |
62 | /// The leftmost boundary of the text region. |
63 | pub x: f32, |
64 | /// The topmost boundary of the text region. |
65 | pub y: f32, |
66 | /// An optional rightmost boundary on the text region. A line of text that exceeds the |
67 | /// max_width is wrapped to the line below. If the width of a glyph is larger than the |
68 | /// max_width, the glyph will overflow past the max_width. The application is responsible for |
69 | /// handling the overflow. |
70 | pub max_width: Option<f32>, |
71 | /// An optional bottom boundary on the text region. This is used for positioning the |
72 | /// vertical_align option. Text that exceeds the defined max_height will overflow past it. The |
73 | /// application is responsible for handling the overflow. |
74 | pub max_height: Option<f32>, |
75 | /// The default is Left. This option does nothing if the max_width isn't set. |
76 | pub horizontal_align: HorizontalAlign, |
77 | /// The default is Top. This option does nothing if the max_height isn't set. |
78 | pub vertical_align: VerticalAlign, |
79 | /// The height of each line as a multiplier of the default. |
80 | pub line_height: f32, |
81 | /// The default is Word. Wrap style is a hint for how strings of text should be wrapped to the |
82 | /// next line. Line wrapping can happen when the max width/height is reached. |
83 | pub wrap_style: WrapStyle, |
84 | /// The default is true. This option enables hard breaks, like new line characters, to |
85 | /// prematurely wrap lines. If false, hard breaks will not prematurely create a new line. |
86 | pub wrap_hard_breaks: bool, |
87 | } |
88 | |
89 | impl Default for LayoutSettings { |
90 | fn default() -> LayoutSettings { |
91 | LayoutSettings { |
92 | x: 0.0, |
93 | y: 0.0, |
94 | max_width: None, |
95 | max_height: None, |
96 | horizontal_align: HorizontalAlign::Left, |
97 | vertical_align: VerticalAlign::Top, |
98 | line_height: 1.0, |
99 | wrap_style: WrapStyle::Word, |
100 | wrap_hard_breaks: true, |
101 | } |
102 | } |
103 | } |
104 | |
105 | /// Configuration for rasterizing a glyph. This struct is also a hashable key that can be used to |
106 | /// uniquely identify a rasterized glyph for applications that want to cache glyphs. |
107 | #[derive (Debug, Copy, Clone)] |
108 | pub struct GlyphRasterConfig { |
109 | /// The glyph index represented by the glyph being positioned. |
110 | pub glyph_index: u16, |
111 | /// The scale of the glyph being positioned in px. |
112 | pub px: f32, |
113 | /// The hash of the font used in layout to raster the glyph. |
114 | pub font_hash: usize, |
115 | } |
116 | |
117 | impl Hash for GlyphRasterConfig { |
118 | fn hash<H: Hasher>(&self, state: &mut H) { |
119 | self.glyph_index.hash(state); |
120 | self.px.to_bits().hash(state); |
121 | self.font_hash.hash(state); |
122 | } |
123 | } |
124 | |
125 | impl PartialEq for GlyphRasterConfig { |
126 | fn eq(&self, other: &Self) -> bool { |
127 | self.glyph_index == other.glyph_index && self.px == other.px && self.font_hash == other.font_hash |
128 | } |
129 | } |
130 | |
131 | impl Eq for GlyphRasterConfig {} |
132 | |
133 | /// A positioned scaled glyph. |
134 | #[derive (Debug, Copy, Clone)] |
135 | pub struct GlyphPosition<U: Copy + Clone = ()> { |
136 | /// Hashable key that can be used to uniquely identify a rasterized glyph. |
137 | pub key: GlyphRasterConfig, |
138 | /// The index of the font used to generate this glyph position. |
139 | pub font_index: usize, |
140 | /// The associated character that generated this glyph. A character may generate multiple |
141 | /// glyphs. |
142 | pub parent: char, |
143 | /// The xmin of the glyph bounding box. This represents the left side of the glyph. Dimensions |
144 | /// are in pixels, and are always whole numbers. |
145 | pub x: f32, |
146 | /// The ymin of the glyph bounding box. If your coordinate system is PositiveYUp, this |
147 | /// represents the bottom side of the glyph. If your coordinate system is PositiveYDown, this |
148 | /// represents the top side of the glyph. This is like this so that (y + height) always produces |
149 | /// the other bound for the glyph. |
150 | pub y: f32, |
151 | /// The width of the glyph. Dimensions are in pixels. |
152 | pub width: usize, |
153 | /// The height of the glyph. Dimensions are in pixels. |
154 | pub height: usize, |
155 | /// The byte offset into the original string used in the append call which created |
156 | /// this glyph. |
157 | pub byte_offset: usize, |
158 | /// Additional metadata associated with the character used to generate this glyph. |
159 | pub char_data: CharacterData, |
160 | /// Custom user data associated with the text styled used to generate this glyph. |
161 | pub user_data: U, |
162 | } |
163 | |
164 | /// A style description for a segment of text. |
165 | pub struct TextStyle<'a, U: Copy + Clone = ()> { |
166 | /// The text to layout. |
167 | pub text: &'a str, |
168 | /// The scale of the text in pixel units. The units of the scale are pixels per Em unit. |
169 | pub px: f32, |
170 | /// The font to layout the text in. |
171 | pub font_index: usize, |
172 | /// Additional user data to associate with glyphs produced by this text style. |
173 | pub user_data: U, |
174 | } |
175 | |
176 | impl<'a> TextStyle<'a> { |
177 | pub fn new(text: &'a str, px: f32, font_index: usize) -> TextStyle<'a> { |
178 | TextStyle { |
179 | text, |
180 | px, |
181 | font_index, |
182 | user_data: (), |
183 | } |
184 | } |
185 | } |
186 | |
187 | impl<'a, U: Copy + Clone> TextStyle<'a, U> { |
188 | pub fn with_user_data(text: &'a str, px: f32, font_index: usize, user_data: U) -> TextStyle<'a, U> { |
189 | TextStyle { |
190 | text, |
191 | px, |
192 | font_index, |
193 | user_data, |
194 | } |
195 | } |
196 | } |
197 | |
198 | /// Metrics about a positioned line. |
199 | #[derive (Debug, Copy, Clone)] |
200 | pub struct LinePosition { |
201 | /// The y coordinate of the baseline of this line, in pixels. |
202 | pub baseline_y: f32, |
203 | /// How much empty space is left at the end of the line before any alignment. If no max width is |
204 | /// specified, f32::MAX is used. |
205 | pub padding: f32, |
206 | /// The highest point that any glyph in the font extends to above the baseline. Typically |
207 | /// positive. If there are multiple styles on this line, this is their max value. |
208 | pub max_ascent: f32, |
209 | /// The lowest point that any glyph in the font extends to below the baseline. Typically |
210 | /// negative. If there are multiple styles on this line, this is their min value. |
211 | pub min_descent: f32, |
212 | /// The gap to leave between the descent of one line and the ascent of the next. This is of |
213 | /// course only a guideline given by the font's designers. If there are multiple styles on this |
214 | /// line, this is their max value. |
215 | pub max_line_gap: f32, |
216 | /// A precalculated value for the of the line depending. It's calculated by: ascent - descent + |
217 | /// line_gap. If there are multiple styles on this line, this is their max value. |
218 | pub max_new_line_size: f32, |
219 | /// The GlyphPosition index of the first glyph in the line. |
220 | pub glyph_start: usize, |
221 | /// The GlyphPosition index of the last glyph in the line. |
222 | pub glyph_end: usize, |
223 | /// The x offset into the first layout pass. |
224 | tracking_x: f32, |
225 | } |
226 | |
227 | impl Default for LinePosition { |
228 | fn default() -> Self { |
229 | LinePosition { |
230 | baseline_y: 0.0, |
231 | padding: 0.0, |
232 | max_ascent: 0.0, |
233 | min_descent: 0.0, |
234 | max_line_gap: 0.0, |
235 | max_new_line_size: 0.0, |
236 | glyph_start: 0, |
237 | glyph_end: 0, |
238 | tracking_x: 0.0, |
239 | } |
240 | } |
241 | } |
242 | |
243 | /// Text layout requires a small amount of heap usage which is contained in the Layout struct. This |
244 | /// context is reused between layout calls. Reusing the Layout struct will greatly reduce memory |
245 | /// allocations and is advisable for performance. |
246 | pub struct Layout<U: Copy + Clone = ()> { |
247 | /// Marks if layout should be performed as if the Y axis is flipped (Positive Y incrementing |
248 | /// down instead of up). |
249 | flip: bool, |
250 | /// Origin position. Left side of the region text is being laid out in. |
251 | x: f32, |
252 | /// Origin position. Top side of the region text is being laid out in. |
253 | y: f32, |
254 | /// A mask to filter only linebreak types being requested. |
255 | wrap_mask: LinebreakData, |
256 | /// The max width of the region text is being laid out in. |
257 | max_width: f32, |
258 | /// The max height of the region text is being laid out in. |
259 | max_height: f32, |
260 | /// A multiplier for how text fills unused vertical space. |
261 | vertical_align: f32, |
262 | /// A multiplier for how text fills unused horizontal space. |
263 | horizontal_align: f32, |
264 | /// A multiplier for the amount of space between lines. |
265 | line_height: f32, |
266 | /// The current height of all laid out text. |
267 | height: f32, |
268 | |
269 | /// Finalized glyph state. |
270 | output: Vec<GlyphPosition<U>>, |
271 | /// Intermediate glyph state. |
272 | glyphs: Vec<GlyphPosition<U>>, |
273 | |
274 | /// Linebreak state. Used to derive linebreaks from past glyphs. |
275 | linebreaker: Linebreaker, |
276 | /// The current highest priority linebreak (Hard > Soft > None). |
277 | linebreak_prev: LinebreakData, |
278 | /// The x position that the glyph that has the current highest priority linebreak status starts |
279 | /// at. |
280 | linebreak_pos: f32, |
281 | /// The index of the glyph that has the current highest priority linebreak status. This glyph is |
282 | /// the last glyph on a line if a linebreak is required. |
283 | linebreak_idx: usize, |
284 | |
285 | /// Layout state of each line currently laid out. This always has at least 1 element. |
286 | line_metrics: Vec<LinePosition>, |
287 | /// The x position the next glyph starts at. |
288 | current_pos: f32, |
289 | /// The ceil(ascent) of the current style. |
290 | current_ascent: f32, |
291 | /// The ceil(descent) of the current style. |
292 | current_descent: f32, |
293 | /// The ceil(line_gap) of the current style. |
294 | current_line_gap: f32, |
295 | /// The ceil(new_line_size) of the current style. |
296 | current_new_line: f32, |
297 | /// The x position the current line starts at. |
298 | start_pos: f32, |
299 | |
300 | /// The settings currently being used for layout. |
301 | settings: LayoutSettings, |
302 | } |
303 | |
304 | impl<'a, U: Copy + Clone> Layout<U> { |
305 | /// Creates a layout instance. This requires the direction that the Y coordinate increases in. |
306 | /// Layout needs to be aware of your coordinate system to place the glyphs correctly. |
307 | pub fn new(coordinate_system: CoordinateSystem) -> Layout<U> { |
308 | let settings = LayoutSettings::default(); |
309 | |
310 | let mut layout = Layout { |
311 | flip: coordinate_system == CoordinateSystem::PositiveYDown, |
312 | x: 0.0, |
313 | y: 0.0, |
314 | wrap_mask: LINEBREAK_NONE, |
315 | max_width: 0.0, |
316 | max_height: 0.0, |
317 | vertical_align: 0.0, |
318 | horizontal_align: 0.0, |
319 | line_height: 1.0, |
320 | output: Vec::new(), |
321 | glyphs: Vec::new(), |
322 | line_metrics: Vec::new(), |
323 | linebreaker: Linebreaker::new(), |
324 | linebreak_prev: LINEBREAK_NONE, |
325 | linebreak_pos: 0.0, |
326 | linebreak_idx: 0, |
327 | current_pos: 0.0, |
328 | current_ascent: 0.0, |
329 | current_descent: 0.0, |
330 | current_line_gap: 0.0, |
331 | current_new_line: 0.0, |
332 | start_pos: 0.0, |
333 | height: 0.0, |
334 | settings, |
335 | }; |
336 | layout.reset(&settings); |
337 | layout |
338 | } |
339 | |
340 | /// Resets the current layout settings and clears all appended text. |
341 | pub fn reset(&mut self, settings: &LayoutSettings) { |
342 | self.settings = *settings; |
343 | self.x = settings.x; |
344 | self.y = settings.y; |
345 | self.wrap_mask = LinebreakData::from_mask( |
346 | settings.wrap_style == WrapStyle::Word, |
347 | settings.wrap_hard_breaks, |
348 | settings.max_width.is_some(), |
349 | ); |
350 | self.max_width = settings.max_width.unwrap_or(core::f32::MAX); |
351 | self.max_height = settings.max_height.unwrap_or(core::f32::MAX); |
352 | self.vertical_align = if settings.max_height.is_none() { |
353 | 0.0 |
354 | } else { |
355 | match settings.vertical_align { |
356 | VerticalAlign::Top => 0.0, |
357 | VerticalAlign::Middle => 0.5, |
358 | VerticalAlign::Bottom => 1.0, |
359 | } |
360 | }; |
361 | self.horizontal_align = if settings.max_width.is_none() { |
362 | 0.0 |
363 | } else { |
364 | match settings.horizontal_align { |
365 | HorizontalAlign::Left => 0.0, |
366 | HorizontalAlign::Center => 0.5, |
367 | HorizontalAlign::Right => 1.0, |
368 | } |
369 | }; |
370 | self.line_height = settings.line_height; |
371 | self.clear(); |
372 | } |
373 | |
374 | /// Keeps current layout settings but clears all appended text. |
375 | pub fn clear(&mut self) { |
376 | self.glyphs.clear(); |
377 | self.output.clear(); |
378 | self.line_metrics.clear(); |
379 | self.line_metrics.push(LinePosition::default()); |
380 | |
381 | self.linebreaker.reset(); |
382 | self.linebreak_prev = LINEBREAK_NONE; |
383 | self.linebreak_pos = 0.0; |
384 | self.linebreak_idx = 0; |
385 | self.current_pos = 0.0; |
386 | self.current_ascent = 0.0; |
387 | self.current_descent = 0.0; |
388 | self.current_line_gap = 0.0; |
389 | self.current_new_line = 0.0; |
390 | self.start_pos = 0.0; |
391 | self.height = 0.0; |
392 | } |
393 | |
394 | /// Gets the current height of the appended text. |
395 | pub fn height(&self) -> f32 { |
396 | if let Some(line) = self.line_metrics.last() { |
397 | self.height + line.max_new_line_size |
398 | } else { |
399 | 0.0 |
400 | } |
401 | } |
402 | |
403 | /// Gets the currently positioned lines. If there are no lines positioned, this returns none. |
404 | pub fn lines(&'a self) -> Option<&'a Vec<LinePosition>> { |
405 | if self.glyphs.is_empty() { |
406 | None |
407 | } else { |
408 | Some(&self.line_metrics) |
409 | } |
410 | } |
411 | |
412 | /// Performs layout for text horizontally, and wrapping vertically. This makes a best effort |
413 | /// attempt at laying out the text defined in the given styles with the provided layout |
414 | /// settings. Text may overflow out of the bounds defined in the layout settings and it's up |
415 | /// to the application to decide how to deal with this. |
416 | /// |
417 | /// Characters from the input string can only be omitted from the output, they are never |
418 | /// reordered. The output buffer will always contain characters in the order they were defined |
419 | /// in the styles. |
420 | pub fn append<T: Borrow<Font>>(&mut self, fonts: &[T], style: &TextStyle<U>) { |
421 | // The first layout pass requires some text. |
422 | if style.text.is_empty() { |
423 | return; |
424 | } |
425 | |
426 | let font: &Font = &fonts[style.font_index].borrow(); |
427 | |
428 | if let Some(metrics) = font.horizontal_line_metrics(style.px) { |
429 | self.current_ascent = ceil(metrics.ascent); |
430 | self.current_new_line = ceil(metrics.new_line_size); |
431 | self.current_descent = ceil(metrics.descent); |
432 | self.current_line_gap = ceil(metrics.line_gap); |
433 | if let Some(line) = self.line_metrics.last_mut() { |
434 | if self.current_ascent > line.max_ascent { |
435 | line.max_ascent = self.current_ascent; |
436 | } |
437 | if self.current_descent < line.min_descent { |
438 | line.min_descent = self.current_descent; |
439 | } |
440 | if self.current_line_gap > line.max_line_gap { |
441 | line.max_line_gap = self.current_line_gap; |
442 | } |
443 | if self.current_new_line > line.max_new_line_size { |
444 | line.max_new_line_size = self.current_new_line; |
445 | } |
446 | } |
447 | } |
448 | |
449 | let mut byte_offset = 0; |
450 | while byte_offset < style.text.len() { |
451 | let prev_byte_offset = byte_offset; |
452 | let character = read_utf8(style.text.as_bytes(), &mut byte_offset); |
453 | let linebreak = self.linebreaker.next(character).mask(self.wrap_mask); |
454 | let glyph_index = font.lookup_glyph_index(character); |
455 | let char_data = CharacterData::classify(character, glyph_index); |
456 | let metrics = if !char_data.is_control() { |
457 | font.metrics_indexed(glyph_index, style.px) |
458 | } else { |
459 | Metrics::default() |
460 | }; |
461 | let advance = ceil(metrics.advance_width); |
462 | |
463 | if linebreak >= self.linebreak_prev { |
464 | self.linebreak_prev = linebreak; |
465 | self.linebreak_pos = self.current_pos; |
466 | self.linebreak_idx = self.glyphs.len().saturating_sub(1); // Mark the previous glyph |
467 | } |
468 | |
469 | // Perform a linebreak |
470 | if linebreak.is_hard() || (self.current_pos - self.start_pos + advance > self.max_width) { |
471 | self.linebreak_prev = LINEBREAK_NONE; |
472 | let mut next_glyph_start = self.glyphs().len(); |
473 | if let Some(line) = self.line_metrics.last_mut() { |
474 | line.glyph_end = self.linebreak_idx; |
475 | line.padding = self.max_width - (self.linebreak_pos - self.start_pos); |
476 | self.height += line.max_new_line_size * self.line_height; |
477 | next_glyph_start = self.linebreak_idx + 1; |
478 | } |
479 | self.line_metrics.push(LinePosition { |
480 | baseline_y: 0.0, |
481 | padding: 0.0, |
482 | max_ascent: self.current_ascent, |
483 | min_descent: self.current_descent, |
484 | max_line_gap: self.current_line_gap, |
485 | max_new_line_size: self.current_new_line, |
486 | glyph_start: next_glyph_start, |
487 | glyph_end: 0, |
488 | tracking_x: self.linebreak_pos, |
489 | }); |
490 | self.start_pos = self.linebreak_pos; |
491 | } |
492 | |
493 | let y = if self.flip { |
494 | floor(-metrics.bounds.height - metrics.bounds.ymin) // PositiveYDown |
495 | } else { |
496 | floor(metrics.bounds.ymin) // PositiveYUp |
497 | }; |
498 | |
499 | self.glyphs.push(GlyphPosition { |
500 | key: GlyphRasterConfig { |
501 | glyph_index: glyph_index as u16, |
502 | px: style.px, |
503 | font_hash: font.file_hash(), |
504 | }, |
505 | font_index: style.font_index, |
506 | parent: character, |
507 | byte_offset: prev_byte_offset, |
508 | x: floor(self.current_pos + metrics.bounds.xmin), |
509 | y, |
510 | width: metrics.width, |
511 | height: metrics.height, |
512 | char_data, |
513 | user_data: style.user_data, |
514 | }); |
515 | self.current_pos += advance; |
516 | } |
517 | |
518 | if let Some(line) = self.line_metrics.last_mut() { |
519 | line.padding = self.max_width - (self.current_pos - self.start_pos); |
520 | line.glyph_end = self.glyphs.len().saturating_sub(1); |
521 | } |
522 | |
523 | self.finalize(); |
524 | } |
525 | |
526 | fn finalize(&mut self) { |
527 | // The second layout pass requires at least 1 glyph to layout. |
528 | if self.glyphs.is_empty() { |
529 | return; |
530 | } |
531 | |
532 | unsafe { self.output.set_len(0) }; |
533 | self.output.reserve(self.glyphs.len()); |
534 | |
535 | let dir = if self.flip { |
536 | -1.0 // PositiveYDown |
537 | } else { |
538 | 1.0 // PositiveYUp |
539 | }; |
540 | |
541 | let mut baseline_y = self.y - dir * floor((self.max_height - self.height()) * self.vertical_align); |
542 | let mut idx = 0; |
543 | for line in &mut self.line_metrics { |
544 | let x_padding = self.x - line.tracking_x + floor(line.padding * self.horizontal_align); |
545 | baseline_y -= dir * line.max_ascent; |
546 | line.baseline_y = baseline_y; |
547 | while idx <= line.glyph_end { |
548 | let mut glyph = self.glyphs[idx]; |
549 | glyph.x += x_padding; |
550 | glyph.y += baseline_y; |
551 | self.output.push(glyph); |
552 | idx += 1; |
553 | } |
554 | baseline_y -= dir * (line.max_new_line_size * self.line_height - line.max_ascent); |
555 | } |
556 | } |
557 | |
558 | /// Gets the currently laid out glyphs. |
559 | pub fn glyphs(&'a self) -> &'a Vec<GlyphPosition<U>> { |
560 | &self.output |
561 | } |
562 | |
563 | /// Gets the settings currently being used for layout. |
564 | pub fn settings(&self) -> &LayoutSettings { |
565 | &self.settings |
566 | } |
567 | } |
568 | |