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}