kas_text/text.rs
1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4// https://www.apache.org/licenses/LICENSE-2.0
5
6//! Text object
7
8use crate::display::{Effect, MarkerPosIter, NotReady, TextDisplay};
9use crate::fonts::{FontSelector, NoFontMatch};
10use crate::format::FormattableText;
11use crate::{Align, Direction, GlyphRun, Line, Status, Vec2};
12use std::num::NonZeroUsize;
13
14/// Text type-setting object (high-level API)
15///
16/// This struct contains:
17/// - A [`FormattableText`]
18/// - A [`TextDisplay`]
19/// - A [`FontSelector`]
20/// - Font size; this defaults to 16px (the web default).
21/// - Text direction and alignment; by default this is inferred from the text.
22/// - Line-wrap width; see [`Text::set_wrap_width`].
23/// - The bounds used for alignment; these [must be set][Text::set_bounds].
24///
25/// This struct tracks the [`TextDisplay`]'s
26/// [state of preparation][TextDisplay#status-of-preparation] and will perform
27/// steps as required. To use this struct:
28/// ```
29/// use kas_text::{Text, Vec2};
30/// use std::path::Path;
31///
32/// let mut text = Text::new("Hello, world!");
33/// text.set_bounds(Vec2(200.0, 50.0));
34/// text.prepare().unwrap();
35///
36/// for run in text.runs(Vec2::ZERO, &[]).unwrap() {
37/// let (face, dpem) = (run.face_id(), run.dpem());
38/// for glyph in run.glyphs() {
39/// println!("{face:?} - {dpem}px - {glyph:?}");
40/// }
41/// }
42/// ```
43#[derive(Clone, Debug)]
44pub struct Text<T: FormattableText + ?Sized> {
45 /// Bounds to use for alignment
46 bounds: Vec2,
47 font: FontSelector,
48 dpem: f32,
49 wrap_width: f32,
50 /// Alignment (`horiz`, `vert`)
51 ///
52 /// By default, horizontal alignment is left or right depending on the
53 /// text direction (see [`Self::direction`]), and vertical alignment
54 /// is to the top.
55 align: (Align, Align),
56 direction: Direction,
57 status: Status,
58
59 display: TextDisplay,
60 text: T,
61}
62
63impl<T: Default + FormattableText> Default for Text<T> {
64 #[inline]
65 fn default() -> Self {
66 Text::new(T::default())
67 }
68}
69
70/// Constructors and other methods requiring `T: Sized`
71impl<T: FormattableText> Text<T> {
72 /// Construct from a text model
73 ///
74 /// This struct must be made ready for usage by calling [`Text::prepare`].
75 #[inline]
76 pub fn new(text: T) -> Self {
77 Text {
78 bounds: Vec2::INFINITY,
79 font: FontSelector::default(),
80 dpem: 16.0,
81 wrap_width: f32::INFINITY,
82 align: Default::default(),
83 direction: Direction::default(),
84 status: Status::New,
85 text,
86 display: Default::default(),
87 }
88 }
89
90 /// Replace the [`TextDisplay`]
91 ///
92 /// This may be used with [`Self::new`] to reconstruct an object which was
93 /// disolved [`into_parts`][Self::into_parts].
94 #[inline]
95 pub fn with_display(mut self, display: TextDisplay) -> Self {
96 self.display = display;
97 self
98 }
99
100 /// Decompose into parts
101 #[inline]
102 pub fn into_parts(self) -> (TextDisplay, T) {
103 (self.display, self.text)
104 }
105
106 /// Clone the formatted text
107 pub fn clone_text(&self) -> T
108 where
109 T: Clone,
110 {
111 self.text.clone()
112 }
113
114 /// Extract text object, discarding the rest
115 #[inline]
116 pub fn take_text(self) -> T {
117 self.text
118 }
119
120 /// Access the formattable text object
121 #[inline]
122 pub fn text(&self) -> &T {
123 &self.text
124 }
125
126 /// Set the text
127 ///
128 /// One must call [`Text::prepare`] afterwards and may wish to inspect its
129 /// return value to check the size allocation meets requirements.
130 pub fn set_text(&mut self, text: T) {
131 if self.text == text {
132 return; // no change
133 }
134
135 self.text = text;
136 self.set_max_status(Status::New);
137 }
138}
139
140/// Text, font and type-setting getters and setters
141impl<T: FormattableText + ?Sized> Text<T> {
142 /// Length of text
143 ///
144 /// This is a shortcut to `self.as_str().len()`.
145 ///
146 /// It is valid to reference text within the range `0..text_len()`,
147 /// even if not all text within this range will be displayed (due to runs).
148 #[inline]
149 pub fn str_len(&self) -> usize {
150 self.as_str().len()
151 }
152
153 /// Access whole text as contiguous `str`
154 ///
155 /// It is valid to reference text within the range `0..text_len()`,
156 /// even if not all text within this range will be displayed (due to runs).
157 #[inline]
158 pub fn as_str(&self) -> &str {
159 self.text.as_str()
160 }
161
162 /// Clone the unformatted text as a `String`
163 #[inline]
164 pub fn clone_string(&self) -> String {
165 self.text.as_str().to_string()
166 }
167
168 /// Get the font selector
169 #[inline]
170 pub fn font(&self) -> FontSelector {
171 self.font
172 }
173
174 /// Set the font selector
175 ///
176 /// This font selector is used by all unformatted texts and by formatted
177 /// texts which don't immediately replace the selector.
178 ///
179 /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
180 #[inline]
181 pub fn set_font(&mut self, font: FontSelector) {
182 if font != self.font {
183 self.font = font;
184 self.set_max_status(Status::New);
185 }
186 }
187
188 /// Get the default font size (pixels)
189 #[inline]
190 pub fn font_size(&self) -> f32 {
191 self.dpem
192 }
193
194 /// Set the default font size (pixels)
195 ///
196 /// This is a scaling factor used to convert font sizes, with units
197 /// `pixels/Em`. Equivalently, this is the line-height in pixels.
198 /// See [`crate::fonts`] documentation.
199 ///
200 /// To calculate this from text size in Points, use `dpem = dpp * pt_size`
201 /// where the dots-per-point is usually `dpp = scale_factor * 96.0 / 72.0`
202 /// on PC platforms, or `dpp = 1` on MacOS (or 2 for retina displays).
203 ///
204 /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
205 #[inline]
206 pub fn set_font_size(&mut self, dpem: f32) {
207 if dpem != self.dpem {
208 self.dpem = dpem;
209 self.set_max_status(Status::ResizeLevelRuns);
210 }
211 }
212
213 /// Set font size
214 ///
215 /// This is an alternative to [`Text::set_font_size`]. It is assumed
216 /// that 72 Points = 1 Inch and the base screen resolution is 96 DPI.
217 /// (Note: MacOS uses a different definition where 1 Point = 1 Pixel.)
218 #[inline]
219 pub fn set_font_size_pt(&mut self, pt_size: f32, scale_factor: f32) {
220 self.set_font_size(pt_size * scale_factor * (96.0 / 72.0));
221 }
222
223 /// Get the base text direction
224 #[inline]
225 pub fn direction(&self) -> Direction {
226 self.direction
227 }
228
229 /// Set the base text direction
230 ///
231 /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
232 #[inline]
233 pub fn set_direction(&mut self, direction: Direction) {
234 if direction != self.direction {
235 self.direction = direction;
236 self.set_max_status(Status::New);
237 }
238 }
239
240 /// Get the text wrap width
241 #[inline]
242 pub fn wrap_width(&self) -> f32 {
243 self.wrap_width
244 }
245
246 /// Set wrap width or disable line wrapping
247 ///
248 /// By default, this is [`f32::INFINITY`] and text lines are not wrapped.
249 /// If set to some positive finite value, text lines will be wrapped at that
250 /// width.
251 ///
252 /// Either way, explicit line-breaks such as `\n` still result in new lines.
253 ///
254 /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
255 #[inline]
256 pub fn set_wrap_width(&mut self, wrap_width: f32) {
257 debug_assert!(wrap_width >= 0.0);
258 if wrap_width != self.wrap_width {
259 self.wrap_width = wrap_width;
260 self.set_max_status(Status::LevelRuns);
261 }
262 }
263
264 /// Get text (horizontal, vertical) alignment
265 #[inline]
266 pub fn align(&self) -> (Align, Align) {
267 self.align
268 }
269
270 /// Set text alignment
271 ///
272 /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
273 #[inline]
274 pub fn set_align(&mut self, align: (Align, Align)) {
275 if align != self.align {
276 if align.0 == self.align.0 {
277 self.set_max_status(Status::Wrapped);
278 } else {
279 self.set_max_status(Status::LevelRuns);
280 }
281 self.align = align;
282 }
283 }
284
285 /// Get text bounds
286 #[inline]
287 pub fn bounds(&self) -> Vec2 {
288 self.bounds
289 }
290
291 /// Set text bounds
292 ///
293 /// These are used for alignment. They are not used for wrapping; see
294 /// instead [`Self::set_wrap_width`].
295 ///
296 /// It is expected that `bounds` are finite.
297 #[inline]
298 pub fn set_bounds(&mut self, bounds: Vec2) {
299 debug_assert!(bounds.is_finite());
300 if bounds != self.bounds {
301 if bounds.0 != self.bounds.0 {
302 self.set_max_status(Status::LevelRuns);
303 } else {
304 self.set_max_status(Status::Wrapped);
305 }
306 self.bounds = bounds;
307 }
308 }
309
310 /// Get the base directionality of the text
311 ///
312 /// This does not require that the text is prepared.
313 pub fn text_is_rtl(&self) -> bool {
314 let cached_is_rtl = match self.line_is_rtl(0) {
315 Ok(None) => Some(self.direction == Direction::Rtl),
316 Ok(Some(is_rtl)) => Some(is_rtl),
317 Err(NotReady) => None,
318 };
319 #[cfg(not(debug_assertions))]
320 if let Some(cached) = cached_is_rtl {
321 return cached;
322 }
323
324 let is_rtl = self.display.text_is_rtl(self.as_str(), self.direction);
325 if let Some(cached) = cached_is_rtl {
326 debug_assert_eq!(cached, is_rtl);
327 }
328 is_rtl
329 }
330
331 /// Get the sequence of effect tokens
332 ///
333 /// This method has some limitations: (1) it may only return a reference to
334 /// an existing sequence, (2) effect tokens cannot be generated dependent
335 /// on input state, and (3) it does not incorporate color information. For
336 /// most uses it should still be sufficient, but for other cases it may be
337 /// preferable not to use this method (use a dummy implementation returning
338 /// `&[]` and use inherent methods on the text object via [`Text::text`]).
339 #[inline]
340 pub fn effect_tokens(&self) -> &[Effect] {
341 self.text.effect_tokens()
342 }
343}
344
345/// Type-setting operations and status
346impl<T: FormattableText + ?Sized> Text<T> {
347 /// Check whether the status is at least `status`
348 #[inline]
349 pub fn check_status(&self, status: Status) -> Result<(), NotReady> {
350 if self.status >= status {
351 Ok(())
352 } else {
353 Err(NotReady)
354 }
355 }
356
357 /// Check whether the text is fully prepared and ready for usage
358 #[inline]
359 pub fn is_prepared(&self) -> bool {
360 self.status == Status::Ready
361 }
362
363 /// Adjust status to indicate a required action
364 ///
365 /// This is used to notify that some step of preparation may need to be
366 /// repeated. The internally-tracked status is set to the minimum of
367 /// `status` and its previous value.
368 #[inline]
369 fn set_max_status(&mut self, status: Status) {
370 self.status = self.status.min(status);
371 }
372
373 /// Read the [`TextDisplay`], without checking status
374 #[inline]
375 pub fn unchecked_display(&self) -> &TextDisplay {
376 &self.display
377 }
378
379 /// Read the [`TextDisplay`], if fully prepared
380 #[inline]
381 pub fn display(&self) -> Result<&TextDisplay, NotReady> {
382 self.check_status(Status::Ready)?;
383 Ok(self.unchecked_display())
384 }
385
386 /// Read the [`TextDisplay`], if at least wrapped
387 #[inline]
388 pub fn wrapped_display(&self) -> Result<&TextDisplay, NotReady> {
389 self.check_status(Status::Wrapped)?;
390 Ok(self.unchecked_display())
391 }
392
393 #[inline]
394 fn prepare_runs(&mut self) -> Result<(), NoFontMatch> {
395 match self.status {
396 Status::New => {
397 self.display
398 .prepare_runs(&self.text, self.direction, self.font, self.dpem)?
399 }
400 Status::ResizeLevelRuns => self.display.resize_runs(&self.text, self.dpem),
401 _ => (),
402 }
403
404 self.status = Status::LevelRuns;
405 Ok(())
406 }
407
408 /// Measure required width, up to some `max_width`
409 ///
410 /// This method partially prepares the [`TextDisplay`] as required.
411 ///
412 /// This method allows calculation of the width requirement of a text object
413 /// without full wrapping and glyph placement. Whenever the requirement
414 /// exceeds `max_width`, the algorithm stops early, returning `max_width`.
415 ///
416 /// The return value is unaffected by alignment and wrap configuration.
417 pub fn measure_width(&mut self, max_width: f32) -> Result<f32, NoFontMatch> {
418 self.prepare_runs()?;
419
420 Ok(self.display.measure_width(max_width))
421 }
422
423 /// Measure required vertical height, wrapping as configured
424 ///
425 /// Stops after `max_lines`, if provided.
426 pub fn measure_height(&mut self, max_lines: Option<NonZeroUsize>) -> Result<f32, NoFontMatch> {
427 if self.status >= Status::Wrapped {
428 let (tl, br) = self.display.bounding_box();
429 return Ok(br.1 - tl.1);
430 }
431
432 self.prepare_runs()?;
433 Ok(self.display.measure_height(self.wrap_width, max_lines))
434 }
435
436 /// Prepare text for display, as necessary
437 ///
438 /// [`Self::set_bounds`] must be called before this method.
439 ///
440 /// Does all preparation steps necessary in order to display or query the
441 /// layout of this text. Text is aligned within the given `bounds`.
442 ///
443 /// Returns `Ok(true)` on success when some action is performed, `Ok(false)`
444 /// when the text is already prepared.
445 pub fn prepare(&mut self) -> Result<bool, NotReady> {
446 if self.is_prepared() {
447 return Ok(false);
448 } else if !self.bounds.is_finite() {
449 return Err(NotReady);
450 }
451
452 self.prepare_runs().unwrap();
453 debug_assert!(self.status >= Status::LevelRuns);
454
455 if self.status == Status::LevelRuns {
456 self.display
457 .prepare_lines(self.wrap_width, self.bounds.0, self.align.0);
458 }
459
460 if self.status <= Status::Wrapped {
461 self.display.vertically_align(self.bounds.1, self.align.1);
462 }
463
464 self.status = Status::Ready;
465 Ok(true)
466 }
467
468 /// Get the size of the required bounding box
469 ///
470 /// This is the position of the upper-left and lower-right corners of a
471 /// bounding box on content.
472 /// Alignment and input bounds do affect the result.
473 #[inline]
474 pub fn bounding_box(&self) -> Result<(Vec2, Vec2), NotReady> {
475 Ok(self.wrapped_display()?.bounding_box())
476 }
477
478 /// Get the number of lines (after wrapping)
479 ///
480 /// See [`TextDisplay::num_lines`].
481 #[inline]
482 pub fn num_lines(&self) -> Result<usize, NotReady> {
483 Ok(self.wrapped_display()?.num_lines())
484 }
485
486 /// Get line properties
487 #[inline]
488 pub fn get_line(&self, index: usize) -> Result<Option<&Line>, NotReady> {
489 Ok(self.wrapped_display()?.get_line(index))
490 }
491
492 /// Iterate over line properties
493 ///
494 /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
495 #[inline]
496 pub fn lines(&self) -> Result<impl Iterator<Item = &Line>, NotReady> {
497 Ok(self.wrapped_display()?.lines())
498 }
499
500 /// Find the line containing text `index`
501 ///
502 /// See [`TextDisplay::find_line`].
503 #[inline]
504 pub fn find_line(
505 &self,
506 index: usize,
507 ) -> Result<Option<(usize, std::ops::Range<usize>)>, NotReady> {
508 Ok(self.wrapped_display()?.find_line(index))
509 }
510
511 /// Get the directionality of the current line
512 ///
513 /// See [`TextDisplay::line_is_rtl`].
514 #[inline]
515 pub fn line_is_rtl(&self, line: usize) -> Result<Option<bool>, NotReady> {
516 Ok(self.wrapped_display()?.line_is_rtl(line))
517 }
518
519 /// Find the text index for the glyph nearest the given `pos`
520 ///
521 /// See [`TextDisplay::text_index_nearest`].
522 #[inline]
523 pub fn text_index_nearest(&self, pos: Vec2) -> Result<usize, NotReady> {
524 Ok(self.display()?.text_index_nearest(pos))
525 }
526
527 /// Find the text index nearest horizontal-coordinate `x` on `line`
528 ///
529 /// See [`TextDisplay::line_index_nearest`].
530 #[inline]
531 pub fn line_index_nearest(&self, line: usize, x: f32) -> Result<Option<usize>, NotReady> {
532 Ok(self.wrapped_display()?.line_index_nearest(line, x))
533 }
534
535 /// Find the starting position (top-left) of the glyph at the given index
536 ///
537 /// See [`TextDisplay::text_glyph_pos`].
538 pub fn text_glyph_pos(&self, index: usize) -> Result<MarkerPosIter, NotReady> {
539 Ok(self.display()?.text_glyph_pos(index))
540 }
541
542 /// Get the number of glyphs
543 ///
544 /// See [`TextDisplay::num_glyphs`].
545 #[inline]
546 #[cfg(feature = "num_glyphs")]
547 pub fn num_glyphs(&self) -> Result<usize, NotReady> {
548 Ok(self.wrapped_display()?.num_glyphs())
549 }
550
551 /// Iterate over runs of positioned glyphs
552 ///
553 /// All glyphs are translated by the given `offset` (this is practically
554 /// free).
555 ///
556 /// An [`Effect`] sequence supports underline, strikethrough and custom
557 /// indexing (e.g. for a color palette). Pass `&[]` if effects are not
558 /// required. (The default effect is always [`Effect::default()`].)
559 ///
560 /// Runs are yielded in undefined order. The total number of
561 /// glyphs yielded will equal [`TextDisplay::num_glyphs`].
562 pub fn runs<'a>(
563 &'a self,
564 offset: Vec2,
565 effects: &'a [Effect],
566 ) -> Result<impl Iterator<Item = GlyphRun<'a>> + 'a, NotReady> {
567 Ok(self.display()?.runs(offset, effects))
568 }
569
570 /// Yield a sequence of rectangles to highlight a given text range
571 ///
572 /// Calls `f(top_left, bottom_right)` for each highlighting rectangle.
573 pub fn highlight_range<F>(
574 &self,
575 range: std::ops::Range<usize>,
576 mut f: F,
577 ) -> Result<(), NotReady>
578 where
579 F: FnMut(Vec2, Vec2),
580 {
581 Ok(self.display()?.highlight_range(range, &mut f))
582 }
583}