Skip to main content

kas_text/display/
mod.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 prepared for display
7
8#[allow(unused)]
9use crate::Text;
10use crate::conv::to_usize;
11use crate::{Direction, Vec2, shaper};
12use smallvec::SmallVec;
13use tinyvec::TinyVec;
14
15mod glyph_pos;
16mod text_runs;
17mod wrap_lines;
18pub use glyph_pos::{Effect, EffectFlags, GlyphRun, MarkerPos, MarkerPosIter};
19pub(crate) use text_runs::RunSpecial;
20pub use wrap_lines::Line;
21use wrap_lines::RunPart;
22
23/// Error returned on operations if not ready
24///
25/// This error is returned if `prepare` must be called.
26#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, thiserror::Error)]
27#[error("not ready")]
28pub struct NotReady;
29
30/// Text type-setting object (low-level, without text and configuration)
31///
32/// This struct caches type-setting data at multiple levels of preparation.
33/// Its end result is a sequence of type-set glyphs.
34///
35/// It is usually recommended to use [`Text`] instead, which includes
36/// the source text, type-setting configuration and status tracking.
37///
38/// ### Status of preparation
39///
40/// Stages of preparation are as follows:
41///
42/// 1.  Ensure all required [fonts](crate::fonts) are loaded.
43/// 2.  Call [`Self::prepare_runs`] to break text into level runs, then shape
44///     these runs into glyph runs (unwrapped but with weak break points).
45///
46///     This method must be called again if the `text`, text `direction` or
47///     `font_id` change. If only the text size (`dpem`) changes, it is
48///     sufficient to instead call [`Self::resize_runs`].
49/// 3.  Optionally, [`Self::measure_width`] and [`Self::measure_height`] may be
50///     used at this point to determine size requirements.
51/// 4.  Call [`Self::prepare_lines`] to wrap text and perform re-ordering (where
52///     lines are bi-directional) and horizontal alignment.
53///
54///     This must be called again if any of `wrap_width`, `width_bound` or
55///     `h_align` change.
56/// 5.  Call [`Self::vertically_align`] to set or adjust vertical alignment.
57///     (Not technically required if alignment is always top.)
58///
59/// All methods are idempotent (that is, they may be called multiple times
60/// without affecting the result). Later stages of preparation do not affect
61/// earlier stages, but if an earlier stage is repeated to account for adjusted
62/// configuration then later stages must also be repeated.
63///
64/// This struct does not track the state of preparation. It is recommended to
65/// use [`Text`] or a custom wrapper for that purpose. Failure to observe the
66/// correct sequence is memory-safe but may cause panic or an unexpected result.
67///
68/// ### Text navigation
69///
70/// Despite lacking a copy of the underlying text, text-indices may be mapped to
71/// glyphs and lines, and vice-versa.
72///
73/// The text range is `0..self.text_len()`. Any index within this range
74/// (inclusive of end point) is valid for usage in all methods taking a text index.
75/// Multiple indices may map to the same glyph (e.g. within multi-byte chars,
76/// with combining-diacritics, and with ligatures). In some cases a single index
77/// corresponds to multiple glyph positions (due to line-wrapping or change of
78/// direction in bi-directional text).
79///
80/// Navigating to the start or end of a line can be done with
81/// [`TextDisplay::find_line`], [`TextDisplay::get_line`] and [`Line::text_range`].
82///
83/// Navigating forwards or backwards should be done via a library such as
84/// [`unicode-segmentation`](https://github.com/unicode-rs/unicode-segmentation)
85/// which provides a
86/// [`GraphemeCursor`](https://unicode-rs.github.io/unicode-segmentation/unicode_segmentation/struct.GraphemeCursor.html)
87/// to step back or forward one "grapheme", in logical text order.
88/// Optionally, the direction may
89/// be reversed for right-to-left lines [`TextDisplay::line_is_rtl`], but note
90/// that the result may be confusing since not all text on the line follows the
91/// line's base direction and adjacent lines may have different directions.
92///
93/// Navigating glyphs left or right in display-order is not currently supported.
94///
95/// To navigate "up" and "down" lines, use [`TextDisplay::text_glyph_pos`] to
96/// get the position of the cursor, [`TextDisplay::find_line`] to get the line
97/// number, then [`TextDisplay::line_index_nearest`] to find the new index.
98#[derive(Clone, Debug)]
99pub struct TextDisplay {
100    // NOTE: typical numbers of elements:
101    // Simple labels: runs=1, wrapped_runs=1, lines=1
102    // Longer texts wrapped over n lines: runs=1, wrapped_runs=n, lines=n
103    // Justified wrapped text: similar, but wrapped_runs is the word count
104    // Simple texts with explicit breaks over n lines: all=n
105    // Single-line bidi text: runs=n, wrapped_runs=n, lines=1
106    // Complex bidi or formatted texts: all=many
107    // Conclusion: SmallVec<[T; 1]> saves allocations in many cases.
108    //
109    /// Level runs within the text, in logical order
110    runs: SmallVec<[shaper::GlyphRun; 1]>,
111    /// Contiguous runs, in logical order
112    ///
113    /// Within a line, runs may not be in visual order due to BIDI reversals.
114    wrapped_runs: TinyVec<[RunPart; 1]>,
115    /// Visual (wrapped) lines, in visual and logical order
116    lines: TinyVec<[Line; 1]>,
117    #[cfg(feature = "num_glyphs")]
118    num_glyphs: u32,
119    l_bound: f32,
120    r_bound: f32,
121}
122
123#[cfg(test)]
124#[test]
125fn size_of_elts() {
126    use std::mem::size_of;
127    assert_eq!(size_of::<TinyVec<[u8; 0]>>(), 24);
128    assert_eq!(size_of::<shaper::GlyphRun>(), 120);
129    assert_eq!(size_of::<RunPart>(), 24);
130    assert_eq!(size_of::<Line>(), 24);
131    #[cfg(not(feature = "num_glyphs"))]
132    assert_eq!(size_of::<TextDisplay>(), 208);
133    #[cfg(feature = "num_glyphs")]
134    assert_eq!(size_of::<TextDisplay>(), 216);
135}
136
137impl Default for TextDisplay {
138    fn default() -> Self {
139        TextDisplay {
140            runs: Default::default(),
141            wrapped_runs: Default::default(),
142            lines: Default::default(),
143            #[cfg(feature = "num_glyphs")]
144            num_glyphs: 0,
145            l_bound: 0.0,
146            r_bound: 0.0,
147        }
148    }
149}
150
151impl TextDisplay {
152    /// Get the number of lines (after wrapping)
153    ///
154    /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
155    #[inline]
156    pub fn num_lines(&self) -> usize {
157        self.lines.len()
158    }
159
160    /// Get line properties
161    ///
162    /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
163    #[inline]
164    pub fn get_line(&self, index: usize) -> Option<&Line> {
165        self.lines.get(index)
166    }
167
168    /// Iterate over line properties
169    ///
170    /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
171    #[inline]
172    pub fn lines(&self) -> impl Iterator<Item = &Line> {
173        self.lines.iter()
174    }
175
176    /// Get the size of the required bounding box
177    ///
178    /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
179    ///
180    /// Returns the position of the upper-left and lower-right corners of a
181    /// bounding box on content.
182    /// Alignment and input bounds do affect the result.
183    pub fn bounding_box(&self) -> (Vec2, Vec2) {
184        if self.lines.is_empty() {
185            return (Vec2::ZERO, Vec2::ZERO);
186        }
187
188        let top = self.lines.first().unwrap().top;
189        let bottom = self.lines.last().unwrap().bottom;
190        (Vec2(self.l_bound, top), Vec2(self.r_bound, bottom))
191    }
192
193    /// Find the line containing text `index`
194    ///
195    /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
196    ///
197    /// Returns the line number and the text-range of the line.
198    ///
199    /// Returns `None` in case `index` does not line on or at the end of a line
200    /// (which means either that `index` is beyond the end of the text or that
201    /// `index` is within a mult-byte line break).
202    pub fn find_line(&self, index: usize) -> Option<(usize, std::ops::Range<usize>)> {
203        let mut first = None;
204        for (n, line) in self.lines.iter().enumerate() {
205            let text_range = line.text_range();
206            if text_range.end == index {
207                // When line wrapping, this also matches the start of the next
208                // line which is the preferred location. At the end of other
209                // lines it does not match any other location.
210                first = Some((n, text_range));
211            } else if text_range.contains(&index) {
212                return Some((n, text_range));
213            }
214        }
215        first
216    }
217
218    /// Get the base directionality of the text
219    ///
220    /// [Requires status][Self#status-of-preparation]: none.
221    pub fn text_is_rtl(&self, text: &str, direction: Direction) -> bool {
222        let (is_auto, mut is_rtl) = match direction {
223            Direction::Ltr => (false, false),
224            Direction::Rtl => (false, true),
225            Direction::Auto => (true, false),
226            Direction::AutoRtl => (true, true),
227        };
228
229        if is_auto {
230            match unicode_bidi::get_base_direction(text) {
231                unicode_bidi::Direction::Ltr => is_rtl = false,
232                unicode_bidi::Direction::Rtl => is_rtl = true,
233                unicode_bidi::Direction::Mixed => (),
234            }
235        }
236
237        is_rtl
238    }
239
240    /// Get the directionality of the current line
241    ///
242    /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
243    ///
244    /// Returns:
245    ///
246    /// - `None` if text is empty
247    /// - `Some(line_is_right_to_left)` otherwise
248    ///
249    /// Note: indeterminate lines (e.g. empty lines) have their direction
250    /// determined from the passed environment, by default left-to-right.
251    pub fn line_is_rtl(&self, line: usize) -> Option<bool> {
252        if let Some(line) = self.lines.get(line) {
253            let first_run = line.run_range.start();
254            let glyph_run = to_usize(self.wrapped_runs[first_run].glyph_run);
255            Some(self.runs[glyph_run].level.is_rtl())
256        } else {
257            None
258        }
259    }
260
261    /// Find the text index for the glyph nearest the given `pos`
262    ///
263    /// [Requires status][Self#status-of-preparation]:
264    /// text is fully prepared for display.
265    ///
266    /// This includes the index immediately after the last glyph, thus
267    /// `result ≤ text.len()`.
268    ///
269    /// Note: if the font's `rect` does not start at the origin, then its top-left
270    /// coordinate should first be subtracted from `pos`.
271    pub fn text_index_nearest(&self, pos: Vec2) -> usize {
272        let mut n = 0;
273        for (i, line) in self.lines.iter().enumerate() {
274            if line.top > pos.1 {
275                break;
276            }
277            n = i;
278        }
279        // Expected to return Some(..) value but None has been observed:
280        self.line_index_nearest(n, pos.0).unwrap_or(0)
281    }
282
283    /// Find the text index nearest horizontal-coordinate `x` on `line`
284    ///
285    /// [Requires status][Self#status-of-preparation]: lines have been wrapped.
286    ///
287    /// This is similar to [`TextDisplay::text_index_nearest`], but allows the
288    /// line to be specified explicitly. Returns `None` only on invalid `line`.
289    pub fn line_index_nearest(&self, line: usize, x: f32) -> Option<usize> {
290        if line >= self.lines.len() {
291            return None;
292        }
293        let line = &self.lines[line];
294        let run_range = line.run_range.to_std();
295
296        let mut best = line.text_range().start;
297        let mut best_dist = f32::INFINITY;
298        let mut try_best = |dist, index: u32| {
299            if dist < best_dist {
300                best = to_usize(index);
301                best_dist = dist;
302            }
303        };
304
305        for run_part in &self.wrapped_runs[run_range] {
306            let glyph_run = &self.runs[to_usize(run_part.glyph_run)];
307            let rel_pos = x - run_part.offset.0;
308
309            let end_index;
310            if glyph_run.level.is_ltr() {
311                for glyph in &glyph_run.glyphs[run_part.glyph_range.to_std()] {
312                    let dist = (glyph.position.0 - rel_pos).abs();
313                    try_best(dist, glyph.index);
314                }
315                end_index = run_part.text_end;
316            } else {
317                let mut index = run_part.text_end;
318                for glyph in &glyph_run.glyphs[run_part.glyph_range.to_std()] {
319                    let dist = (glyph.position.0 - rel_pos).abs();
320                    try_best(dist, index);
321                    index = glyph.index
322                }
323                end_index = index;
324            }
325
326            let end_pos = if run_part.glyph_range.end() < glyph_run.glyphs.len() {
327                glyph_run.glyphs[run_part.glyph_range.end()].position.0
328            } else {
329                glyph_run.caret
330            };
331            try_best((end_pos - rel_pos).abs(), end_index);
332        }
333
334        Some(best)
335    }
336}