kas_core/theme/
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//! Theme-applied Text element
7
8use super::TextClass;
9#[allow(unused)] use super::{DrawCx, SizeCx};
10use crate::cast::Cast;
11#[allow(unused)] use crate::event::ConfigCx;
12use crate::geom::{Rect, Vec2};
13use crate::layout::{AlignHints, AxisInfo, SizeRules};
14use crate::text::fonts::FontSelector;
15use crate::text::format::FormattableText;
16use crate::text::*;
17use crate::{Action, Layout};
18
19/// Text type-setting object (theme aware)
20///
21/// This struct contains:
22/// -   A [`FormattableText`]
23/// -   A [`TextDisplay`]
24/// -   A [`FontSelector`]
25/// -   Type-setting configuration. Values have reasonable defaults:
26///     -   The font is derived from the [`TextClass`] by
27///         [`ConfigCx::text_configure`]. Otherwise, the default font will be
28///         the first loaded font: see [`crate::text::fonts`].
29///     -   The font size is derived from the [`TextClass`] by
30///         [`ConfigCx::text_configure`]. Otherwise, the default font size is
31///         16px (the web default).
32///     -   Default text direction and alignment is inferred from the text.
33///
34/// This struct tracks the [`TextDisplay`]'s
35/// [state of preparation][TextDisplay#status-of-preparation] and will perform
36/// steps as required. Normal usage of this struct is as follows:
37/// -   Configure by calling [`ConfigCx::text_configure`]
38/// -   (Optionally) check size requirements by calling [`SizeCx::text_rules`]
39/// -   Set the size and prepare by calling [`Self::set_rect`]
40/// -   Draw by calling [`DrawCx::text`] (and/or other text methods)
41///
42/// The size according to [`Self::rect`] may be adjusted to that of
43/// the text; see [`Self::set_align`].
44#[derive(Clone, Debug)]
45pub struct Text<T: FormattableText> {
46    rect: Rect,
47    font: FontSelector,
48    dpem: f32,
49    class: TextClass,
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    fn default() -> Self {
65        Self::new(T::default(), TextClass::Label(true))
66    }
67}
68
69/// Implement [`Layout`], using default alignment where alignment is not provided
70impl<T: FormattableText> Layout for Text<T> {
71    fn rect(&self) -> Rect {
72        self.rect
73    }
74
75    fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
76        sizer.text_rules(self, axis)
77    }
78
79    fn set_rect(&mut self, _: &mut ConfigCx, rect: Rect, hints: AlignHints) {
80        self.set_align(hints.complete_default().into());
81        if rect.size != self.rect.size {
82            if rect.size.0 != self.rect.size.0 {
83                self.set_max_status(Status::LevelRuns);
84            } else {
85                self.set_max_status(Status::Wrapped);
86            }
87        }
88        self.rect = rect;
89        self.prepare();
90    }
91
92    fn draw(&self, mut draw: DrawCx) {
93        draw.text(self.rect, self);
94    }
95}
96
97impl<T: FormattableText> Text<T> {
98    /// Construct from a text model
99    ///
100    /// This struct must be made ready for usage by calling [`Text::prepare`].
101    #[inline]
102    pub fn new(text: T, class: TextClass) -> Self {
103        Text {
104            rect: Rect::default(),
105            font: FontSelector::default(),
106            dpem: 16.0,
107            class,
108            align: Default::default(),
109            direction: Direction::default(),
110            status: Status::New,
111            text,
112            display: Default::default(),
113        }
114    }
115
116    /// Replace the [`TextDisplay`]
117    ///
118    /// This may be used with [`Self::new`] to reconstruct an object which was
119    /// disolved [`into_parts`][Self::into_parts].
120    #[inline]
121    pub fn with_display(mut self, display: TextDisplay) -> Self {
122        self.display = display;
123        self
124    }
125
126    /// Decompose into parts
127    #[inline]
128    pub fn into_parts(self) -> (TextDisplay, T) {
129        (self.display, self.text)
130    }
131
132    /// Set text class (inline)
133    ///
134    /// `TextClass::Edit(false)` has special handling: line wrapping is disabled
135    /// and the width of self is set to that of the text.
136    ///
137    /// Default: `TextClass::Label(true)`
138    #[inline]
139    pub fn with_class(mut self, class: TextClass) -> Self {
140        self.class = class;
141        self
142    }
143
144    /// Clone the formatted text
145    pub fn clone_text(&self) -> T
146    where
147        T: Clone,
148    {
149        self.text.clone()
150    }
151
152    /// Extract text object, discarding the rest
153    #[inline]
154    pub fn take_text(self) -> T {
155        self.text
156    }
157
158    /// Access the formattable text object
159    #[inline]
160    pub fn text(&self) -> &T {
161        &self.text
162    }
163
164    /// Set the text
165    ///
166    /// One must call [`Text::prepare`] afterwards and may wish to inspect its
167    /// return value to check the size allocation meets requirements.
168    pub fn set_text(&mut self, text: T) {
169        if self.text == text {
170            return; // no change
171        }
172
173        self.text = text;
174        self.set_max_status(Status::New);
175    }
176
177    /// Length of text
178    ///
179    /// This is a shortcut to `self.as_str().len()`.
180    ///
181    /// It is valid to reference text within the range `0..text_len()`,
182    /// even if not all text within this range will be displayed (due to runs).
183    #[inline]
184    pub fn str_len(&self) -> usize {
185        self.as_str().len()
186    }
187
188    /// Access whole text as contiguous `str`
189    ///
190    /// It is valid to reference text within the range `0..text_len()`,
191    /// even if not all text within this range will be displayed (due to runs).
192    #[inline]
193    pub fn as_str(&self) -> &str {
194        self.text.as_str()
195    }
196
197    /// Clone the unformatted text as a `String`
198    #[inline]
199    pub fn clone_string(&self) -> String {
200        self.text.as_str().to_string()
201    }
202
203    /// Get text class
204    #[inline]
205    pub fn class(&self) -> TextClass {
206        self.class
207    }
208
209    /// Set text class
210    ///
211    /// This controls line-wrapping, font and font size selection.
212    ///
213    /// `TextClass::Edit(false)` has special handling: line wrapping is disabled
214    /// and the width of self is set to that of the text.
215    ///
216    /// Default: `TextClass::Label(true)`
217    #[inline]
218    pub fn set_class(&mut self, class: TextClass) {
219        self.class = class;
220    }
221
222    /// Get the default font
223    #[inline]
224    pub fn font(&self) -> FontSelector {
225        self.font
226    }
227
228    /// Set the default [`FontSelector`]
229    ///
230    /// This is derived from the [`TextClass`] by [`ConfigCx::text_configure`].
231    ///
232    /// This `font` is used by all unformatted texts and by any formatted
233    /// texts which don't immediately set formatting.
234    ///
235    /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
236    #[inline]
237    pub fn set_font(&mut self, font: FontSelector) {
238        if font != self.font {
239            self.font = font;
240            self.set_max_status(Status::New);
241        }
242    }
243
244    /// Get the default font size (pixels)
245    #[inline]
246    pub fn font_size(&self) -> f32 {
247        self.dpem
248    }
249
250    /// Set the default font size (pixels)
251    ///
252    /// This is derived from the [`TextClass`] by [`ConfigCx::text_configure`].
253    ///
254    /// This is a scaling factor used to convert font sizes, with units
255    /// `pixels/Em`. Equivalently, this is the line-height in pixels.
256    /// See [`crate::text::fonts`] documentation.
257    ///
258    /// To calculate this from text size in Points, use `dpem = dpp * pt_size`
259    /// where the dots-per-point is usually `dpp = scale_factor * 96.0 / 72.0`
260    /// on PC platforms, or `dpp = 1` on MacOS (or 2 for retina displays).
261    ///
262    /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
263    #[inline]
264    pub fn set_font_size(&mut self, dpem: f32) {
265        if dpem != self.dpem {
266            self.dpem = dpem;
267            self.set_max_status(Status::ResizeLevelRuns);
268        }
269    }
270
271    /// Set font size
272    ///
273    /// This is an alternative to [`Text::set_font_size`]. It is assumed
274    /// that 72 Points = 1 Inch and the base screen resolution is 96 DPI.
275    /// (Note: MacOS uses a different definition where 1 Point = 1 Pixel.)
276    #[inline]
277    pub fn set_font_size_pt(&mut self, pt_size: f32, scale_factor: f32) {
278        self.set_font_size(pt_size * scale_factor * (96.0 / 72.0));
279    }
280
281    /// Get the base text direction
282    #[inline]
283    pub fn direction(&self) -> Direction {
284        self.direction
285    }
286
287    /// Set the base text direction
288    ///
289    /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
290    #[inline]
291    pub fn set_direction(&mut self, direction: Direction) {
292        if direction != self.direction {
293            self.direction = direction;
294            self.set_max_status(Status::New);
295        }
296    }
297
298    /// Get text (horizontal, vertical) alignment
299    #[inline]
300    pub fn align(&self) -> (Align, Align) {
301        self.align
302    }
303
304    /// Set text alignment
305    ///
306    /// When vertical alignment is [`Align::Default`], [`Self::prepare`] will
307    /// set the vertical size of this [`Layout`] to that of the text.
308    ///
309    /// It is necessary to [`prepare`][Self::prepare] the text after calling this.
310    #[inline]
311    pub fn set_align(&mut self, align: (Align, Align)) {
312        if align != self.align {
313            if align.0 == self.align.0 {
314                self.set_max_status(Status::Wrapped);
315            } else {
316                self.set_max_status(Status::LevelRuns);
317            }
318            self.align = align;
319        }
320    }
321
322    /// Get the base directionality of the text
323    ///
324    /// This does not require that the text is prepared.
325    pub fn text_is_rtl(&self) -> bool {
326        let cached_is_rtl = match self.line_is_rtl(0) {
327            Ok(None) => Some(self.direction == Direction::Rtl),
328            Ok(Some(is_rtl)) => Some(is_rtl),
329            Err(NotReady) => None,
330        };
331        #[cfg(not(debug_assertions))]
332        if let Some(cached) = cached_is_rtl {
333            return cached;
334        }
335
336        let is_rtl = self.display.text_is_rtl(self.as_str(), self.direction);
337        if let Some(cached) = cached_is_rtl {
338            debug_assert_eq!(cached, is_rtl);
339        }
340        is_rtl
341    }
342
343    /// Get the sequence of effect tokens
344    ///
345    /// This method has some limitations: (1) it may only return a reference to
346    /// an existing sequence, (2) effect tokens cannot be generated dependent
347    /// on input state, and (3) it does not incorporate color information. For
348    /// most uses it should still be sufficient, but for other cases it may be
349    /// preferable not to use this method (use a dummy implementation returning
350    /// `&[]` and use inherent methods on the text object via [`Text::text`]).
351    #[inline]
352    pub fn effect_tokens(&self) -> &[Effect] {
353        self.text.effect_tokens()
354    }
355}
356
357/// Type-setting operations and status
358impl<T: FormattableText> Text<T> {
359    /// Check whether the status is at least `status`
360    #[inline]
361    pub fn check_status(&self, status: Status) -> Result<(), NotReady> {
362        if self.status >= status { Ok(()) } else { Err(NotReady) }
363    }
364
365    /// Check whether the text is fully prepared and ready for usage
366    #[inline]
367    pub fn is_prepared(&self) -> bool {
368        self.status == Status::Ready
369    }
370
371    /// Adjust status to indicate a required action
372    ///
373    /// This is used to notify that some step of preparation may need to be
374    /// repeated. The internally-tracked status is set to the minimum of
375    /// `status` and its previous value.
376    #[inline]
377    fn set_max_status(&mut self, status: Status) {
378        self.status = self.status.min(status);
379    }
380
381    /// Read the [`TextDisplay`], without checking status
382    #[inline]
383    pub fn unchecked_display(&self) -> &TextDisplay {
384        &self.display
385    }
386
387    /// Read the [`TextDisplay`], if fully prepared
388    #[inline]
389    pub fn display(&self) -> Result<&TextDisplay, NotReady> {
390        self.check_status(Status::Ready)?;
391        Ok(self.unchecked_display())
392    }
393
394    /// Read the [`TextDisplay`], if at least wrapped
395    #[inline]
396    pub fn wrapped_display(&self) -> Result<&TextDisplay, NotReady> {
397        self.check_status(Status::Wrapped)?;
398        Ok(self.unchecked_display())
399    }
400
401    fn prepare_runs(&mut self) {
402        match self.status {
403            Status::New => self
404                .display
405                .prepare_runs(&self.text, self.direction, self.font, self.dpem)
406                .expect("no suitable font found"),
407            Status::ResizeLevelRuns => self.display.resize_runs(&self.text, self.dpem),
408            _ => (),
409        }
410
411        self.status = Status::LevelRuns;
412    }
413
414    /// Measure required width, up to some `max_width`
415    ///
416    /// This method partially prepares the [`TextDisplay`] as required.
417    ///
418    /// This method allows calculation of the width requirement of a text object
419    /// without full wrapping and glyph placement. Whenever the requirement
420    /// exceeds `max_width`, the algorithm stops early, returning `max_width`.
421    ///
422    /// The return value is unaffected by alignment and wrap configuration.
423    pub fn measure_width(&mut self, max_width: f32) -> f32 {
424        self.prepare_runs();
425        self.display.measure_width(max_width)
426    }
427
428    /// Measure required vertical height
429    ///
430    /// May partially prepare the text for display, but does not otherwise
431    /// modify `self`.
432    pub fn measure_height(&mut self, wrap_width: f32) -> f32 {
433        if self.status >= Status::Wrapped {
434            let (tl, br) = self.display.bounding_box();
435            return br.1 - tl.1;
436        }
437
438        self.prepare_runs();
439        self.display.measure_height(wrap_width)
440    }
441
442    /// Prepare text for display, as necessary
443    ///
444    /// [`Self::set_rect`] must be called before this method.
445    ///
446    /// Does all preparation steps necessary in order to display or query the
447    /// layout of this text. Text is aligned within the set [`Rect`].
448    ///
449    /// Returns `true` on success when some action is performed, `false`
450    /// when the text is already prepared.
451    pub fn prepare(&mut self) -> bool {
452        if self.is_prepared() {
453            return false;
454        }
455
456        self.prepare_runs();
457        debug_assert!(self.status >= Status::LevelRuns);
458
459        if self.status == Status::LevelRuns {
460            let align_width = self.rect.size.0.cast();
461            let wrap_width = if self.class.single_line() {
462                f32::INFINITY
463            } else {
464                align_width
465            };
466            self.display
467                .prepare_lines(wrap_width, align_width, self.align.0);
468        }
469
470        if self.status <= Status::Wrapped {
471            self.display
472                .vertically_align(self.rect.size.1.cast(), self.align.1);
473        }
474
475        self.status = Status::Ready;
476        true
477    }
478
479    /// Re-prepare, if previously prepared, and return an [`Action`]
480    ///
481    /// Wraps [`Text::prepare`], returning an appropriate [`Action`]:
482    ///
483    /// -   When this `Text` object was previously prepared and has sufficient
484    ///     size, it is updated and [`Action::REDRAW`] is returned
485    /// -   When this `Text` object was previously prepared but does not have
486    ///     sufficient size, it is updated and [`Action::RESIZE`] is returned
487    /// -   When this `Text` object was not previously prepared,
488    ///     [`Action::empty()`] is returned without updating `self`.
489    ///
490    /// This is typically called after updating a `Text` object in a widget.
491    pub fn reprepare_action(&mut self) -> Action {
492        match self.prepare() {
493            false => Action::REDRAW,
494            true => {
495                let (tl, br) = self.display.bounding_box();
496                let bounds: Vec2 = self.rect.size.cast();
497                if tl.0 < 0.0 || tl.1 < 0.0 || br.0 > bounds.0 || br.1 > bounds.1 {
498                    Action::RESIZE
499                } else {
500                    Action::REDRAW
501                }
502            }
503        }
504    }
505
506    /// Offset prepared content to avoid left-overhangs
507    ///
508    /// This might be called after [`Self::prepare`] to ensure content does not
509    /// overhang to the left (i.e. that the x-component of the first [`Vec2`]
510    /// returned by [`Self::bounding_box`] is not negative).
511    ///
512    /// This is a special utility intended for content which may be scrolled
513    /// using the size reported by [`Self::bounding_box`]. Note that while
514    /// vertical alignment is untouched by this method, text is never aligned
515    /// above the top (the first y-component is never negative).
516    pub fn ensure_no_left_overhang(&mut self) {
517        if let Ok((tl, _)) = self.bounding_box()
518            && tl.0 < 0.0
519        {
520            self.display.apply_offset(kas_text::Vec2(-tl.0, 0.0));
521        }
522    }
523
524    /// Get the size of the required bounding box
525    ///
526    /// This is the position of the upper-left and lower-right corners of a
527    /// bounding box on content.
528    /// Alignment and size do affect the result.
529    #[inline]
530    pub fn bounding_box(&self) -> Result<(Vec2, Vec2), NotReady> {
531        let (tl, br) = self.wrapped_display()?.bounding_box();
532        Ok((tl.into(), br.into()))
533    }
534
535    /// Get the number of lines (after wrapping)
536    ///
537    /// See [`TextDisplay::num_lines`].
538    #[inline]
539    pub fn num_lines(&self) -> Result<usize, NotReady> {
540        Ok(self.wrapped_display()?.num_lines())
541    }
542
543    /// Find the line containing text `index`
544    ///
545    /// See [`TextDisplay::find_line`].
546    #[inline]
547    pub fn find_line(
548        &self,
549        index: usize,
550    ) -> Result<Option<(usize, std::ops::Range<usize>)>, NotReady> {
551        Ok(self.wrapped_display()?.find_line(index))
552    }
553
554    /// Get the range of a line, by line number
555    ///
556    /// See [`TextDisplay::line_range`].
557    #[inline]
558    pub fn line_range(&self, line: usize) -> Result<Option<std::ops::Range<usize>>, NotReady> {
559        Ok(self.wrapped_display()?.line_range(line))
560    }
561
562    /// Get the directionality of the current line
563    ///
564    /// See [`TextDisplay::line_is_rtl`].
565    #[inline]
566    pub fn line_is_rtl(&self, line: usize) -> Result<Option<bool>, NotReady> {
567        Ok(self.wrapped_display()?.line_is_rtl(line))
568    }
569
570    /// Find the text index for the glyph nearest the given `pos`
571    ///
572    /// See [`TextDisplay::text_index_nearest`].
573    #[inline]
574    pub fn text_index_nearest(&self, pos: Vec2) -> Result<usize, NotReady> {
575        Ok(self.display()?.text_index_nearest(pos.into()))
576    }
577
578    /// Find the text index nearest horizontal-coordinate `x` on `line`
579    ///
580    /// See [`TextDisplay::line_index_nearest`].
581    #[inline]
582    pub fn line_index_nearest(&self, line: usize, x: f32) -> Result<Option<usize>, NotReady> {
583        Ok(self.wrapped_display()?.line_index_nearest(line, x))
584    }
585
586    /// Find the starting position (top-left) of the glyph at the given index
587    ///
588    /// See [`TextDisplay::text_glyph_pos`].
589    pub fn text_glyph_pos(&self, index: usize) -> Result<MarkerPosIter, NotReady> {
590        Ok(self.display()?.text_glyph_pos(index))
591    }
592}
593
594/// Text editing operations
595impl Text<String> {
596    /// Insert a char at the given position
597    ///
598    /// This may be used to edit the raw text instead of replacing it.
599    /// One must call [`Text::prepare`] afterwards.
600    ///
601    /// Currently this is not significantly more efficient than
602    /// [`Text::set_text`]. This may change in the future (TODO).
603    #[inline]
604    pub fn insert_char(&mut self, index: usize, c: char) {
605        self.text.insert(index, c);
606        self.set_max_status(Status::New);
607    }
608
609    /// Replace a section of text
610    ///
611    /// This may be used to edit the raw text instead of replacing it.
612    /// One must call [`Text::prepare`] afterwards.
613    ///
614    /// One may simulate an unbounded range by via `start..usize::MAX`.
615    ///
616    /// Currently this is not significantly more efficient than
617    /// [`Text::set_text`]. This may change in the future (TODO).
618    #[inline]
619    pub fn replace_range(&mut self, range: std::ops::Range<usize>, replace_with: &str) {
620        self.text.replace_range(range, replace_with);
621        self.set_max_status(Status::New);
622    }
623
624    /// Set text to a raw `String`
625    ///
626    /// Returns `true` when new `text` contents do not match old contents. In
627    /// this case the new `text` is assigned, but the caller must also call
628    /// [`Text::prepare`] afterwards.
629    #[inline]
630    pub fn set_string(&mut self, text: String) -> bool {
631        if self.text.as_str() == text {
632            return false; // no change
633        }
634
635        self.text = text;
636        self.set_max_status(Status::New);
637        true
638    }
639
640    /// Swap the raw text with a `String`
641    ///
642    /// This may be used to edit the raw text instead of replacing it.
643    /// One must call [`Text::prepare`] afterwards.
644    ///
645    /// Currently this is not significantly more efficient than
646    /// [`Text::set_text`]. This may change in the future (TODO).
647    #[inline]
648    pub fn swap_string(&mut self, string: &mut String) {
649        std::mem::swap(&mut self.text, string);
650        self.set_max_status(Status::New);
651    }
652}
653
654/// Required functionality on [`Text`] objects for sizing by the theme
655pub trait SizableText {
656    /// Set font face and size
657    fn set_font(&mut self, font: FontSelector, dpem: f32);
658
659    /// Measure required width, up to some `max_width`
660    fn measure_width(&mut self, max_width: f32) -> f32;
661
662    /// Measure required vertical height, wrapping as configured
663    fn measure_height(&mut self, wrap_width: f32) -> f32;
664}
665
666impl<T: FormattableText> SizableText for Text<T> {
667    fn set_font(&mut self, font: FontSelector, dpem: f32) {
668        if font != self.font {
669            self.font = font;
670            self.dpem = dpem;
671            self.set_max_status(Status::New);
672        } else if dpem != self.dpem {
673            self.dpem = dpem;
674            self.set_max_status(Status::ResizeLevelRuns);
675        }
676    }
677
678    fn measure_width(&mut self, max_width: f32) -> f32 {
679        Text::measure_width(self, max_width)
680    }
681
682    fn measure_height(&mut self, wrap_width: f32) -> f32 {
683        Text::measure_height(self, wrap_width)
684    }
685}