Skip to main content

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