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