Skip to main content

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}