1pub use crate::unicode::CharacterData;
2
3use crate::unicode::{read_utf8, LinebreakData, Linebreaker, LINEBREAK_NONE};
4use crate::Font;
5use crate::{
6 platform::{ceil, floor},
7 Metrics,
8};
9use alloc::vec::*;
10use core::borrow::Borrow;
11use core::hash::{Hash, Hasher};
12
13/// Horizontal alignment options for text when a max_width is provided.
14#[derive(Copy, Clone, PartialEq)]
15pub 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)]
26pub 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)]
38pub 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)]
49pub 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)]
61pub 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
89impl 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)]
108pub 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
117impl 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
125impl 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
131impl Eq for GlyphRasterConfig {}
132
133/// A positioned scaled glyph.
134#[derive(Debug, Copy, Clone)]
135pub 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.
165pub 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
176impl<'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
187impl<'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)]
200pub 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
227impl 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.
246pub 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
304impl<'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