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