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}