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}