Skip to main content

kas_text/display/
text_runs.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 preparation: line breaking and BIDI
7
8#![allow(clippy::unnecessary_unwrap)]
9
10use super::TextDisplay;
11use crate::conv::{to_u32, to_usize};
12use crate::fonts::{self, FontSelector, NoFontMatch};
13use crate::format::FormattableText;
14use crate::{Direction, Range, script_to_fontique, shaper};
15use swash::text::LineBreak as LB;
16use swash::text::cluster::Boundary;
17use unicode_bidi::{BidiInfo, LTR_LEVEL, RTL_LEVEL};
18
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub(crate) enum RunSpecial {
21    None,
22    /// Run ends with a hard break
23    HardBreak,
24    /// Run does not end with a break
25    NoBreak,
26    /// Run is a horizontal tab (run is a single char only)
27    HTab,
28}
29
30impl TextDisplay {
31    /// Update font size
32    ///
33    /// [Requires status][Self#status-of-preparation]: level runs have been
34    /// prepared and are valid in all ways except size (`dpem`).
35    ///
36    /// This updates the result of [`TextDisplay::prepare_runs`] due to change
37    /// in font size.
38    pub fn resize_runs<F: FormattableText + ?Sized>(&mut self, text: &F, mut dpem: f32) {
39        let mut font_tokens = text.font_tokens(dpem);
40        let mut next_fmt = font_tokens.next();
41
42        let text = text.as_str();
43
44        for run in &mut self.runs {
45            while let Some(fmt) = next_fmt.as_ref() {
46                if fmt.start > run.range.start {
47                    break;
48                }
49                dpem = fmt.dpem;
50                next_fmt = font_tokens.next();
51            }
52
53            let input = shaper::Input {
54                text,
55                dpem,
56                level: run.level,
57                script: run.script,
58            };
59            let mut breaks = Default::default();
60            std::mem::swap(&mut breaks, &mut run.breaks);
61            if run.level.is_rtl() {
62                breaks.reverse();
63            }
64            *run = shaper::shape(input, run.range, run.face_id, breaks, run.special);
65        }
66    }
67
68    /// Resolve font face and shape run
69    ///
70    /// This may sub-divide text as required to find matching fonts.
71    fn push_run(
72        &mut self,
73        font: FontSelector,
74        input: shaper::Input,
75        range: Range,
76        mut breaks: tinyvec::TinyVec<[shaper::GlyphBreak; 4]>,
77        special: RunSpecial,
78        first_real: Option<char>,
79    ) -> Result<(), NoFontMatch> {
80        let fonts = fonts::library();
81        let font_id = fonts.select_font(&font, input.script)?;
82        let text = &input.text[range.to_std()];
83
84        // Find a font face
85        let mut face_id = None;
86        if let Some(c) = first_real {
87            face_id = fonts
88                .face_for_char(font_id, None, c)
89                .expect("invalid FontId");
90        }
91
92        let mut face = match face_id {
93            Some(id) => id,
94            None => {
95                // We failed to find a font face for the run
96                fonts.first_face_for(font_id).expect("invalid FontId")
97            }
98        };
99
100        let mut start = 0;
101        for (index, c) in text.char_indices() {
102            let index = to_u32(index);
103            if let Some(new_face) = fonts
104                .face_for_char(font_id, Some(face), c)
105                .expect("invalid FontId")
106                && new_face != face
107            {
108                if index > start {
109                    let sub_range = Range {
110                        start: range.start + start,
111                        end: range.start + index,
112                    };
113                    let mut j = 0;
114                    for i in 0..breaks.len() {
115                        if breaks[i].index < sub_range.end {
116                            j = i + 1;
117                        }
118                    }
119                    let rest = breaks.split_off(j);
120
121                    self.runs
122                        .push(shaper::shape(input, sub_range, face, breaks, special));
123                    breaks = rest;
124                    start = index;
125                }
126
127                face = new_face;
128            }
129        }
130
131        let sub_range = Range {
132            start: range.start + start,
133            end: range.end,
134        };
135        self.runs
136            .push(shaper::shape(input, sub_range, face, breaks, special));
137        Ok(())
138    }
139
140    /// Break text into level runs
141    ///
142    /// [Requires status][Self#status-of-preparation]: none.
143    ///
144    /// Must be called again if any of `text`, `direction` or `font` change.
145    /// If only `dpem` changes, [`Self::resize_runs`] may be called instead.
146    ///
147    /// The text is broken into a set of contiguous "level runs". These runs are
148    /// maximal slices of the `text` which do not contain explicit line breaks
149    /// and have a single text direction according to the
150    /// [Unicode Bidirectional Algorithm](http://www.unicode.org/reports/tr9/).
151    pub fn prepare_runs<F: FormattableText + ?Sized>(
152        &mut self,
153        text: &F,
154        direction: Direction,
155        mut font: FontSelector,
156        mut dpem: f32,
157    ) -> Result<(), NoFontMatch> {
158        // This method constructs a list of "hard lines" (the initial line and any
159        // caused by a hard break), each composed of a list of "level runs" (the
160        // result of splitting and reversing according to Unicode TR9 aka
161        // Bidirectional algorithm), plus a list of "soft break" positions
162        // (where wrapping may introduce new lines depending on available space).
163
164        self.runs.clear();
165
166        let mut font_tokens = text.font_tokens(dpem);
167        let mut next_fmt = font_tokens.next();
168        if let Some(fmt) = next_fmt.as_ref()
169            && fmt.start == 0
170        {
171            font = fmt.font;
172            dpem = fmt.dpem;
173            next_fmt = font_tokens.next();
174        }
175
176        let text = text.as_str();
177
178        let default_para_level = match direction {
179            Direction::Auto => None,
180            Direction::AutoRtl => {
181                use unicode_bidi::Direction::*;
182                match unicode_bidi::get_base_direction(text) {
183                    Ltr | Rtl => None,
184                    Mixed => Some(RTL_LEVEL),
185                }
186            }
187            Direction::Ltr => Some(LTR_LEVEL),
188            Direction::Rtl => Some(RTL_LEVEL),
189        };
190        let info = BidiInfo::new(text, default_para_level);
191        let levels = info.levels;
192        assert_eq!(text.len(), levels.len());
193
194        let mut input = shaper::Input {
195            text,
196            dpem,
197            level: levels.first().cloned().unwrap_or(LTR_LEVEL),
198            script: UNKNOWN_SCRIPT,
199        };
200
201        let mut start = 0;
202        let mut breaks = Default::default();
203
204        let mut analyzer = swash::text::analyze(text.chars());
205        let mut last_props = None;
206        let mut first_real = None;
207
208        let mut last_is_control = false;
209        let mut last_is_htab = false;
210        let mut non_control_end = 0;
211
212        for (index, c) in text.char_indices() {
213            // Handling for control chars
214            if !last_is_control {
215                non_control_end = index;
216            }
217            let is_control = c.is_control();
218            let is_htab = c == '\t';
219            let control_break = is_htab || (last_is_control && !is_control);
220
221            let (props, boundary) = analyzer.next().unwrap();
222            last_props = Some(props);
223
224            // Forcibly end the line?
225            let hard_break = boundary == Boundary::Mandatory;
226            // Is wrapping allowed at this position?
227            let is_break = hard_break || boundary == Boundary::Line;
228
229            // Force end of current run?
230            let bidi_break = levels[index] != input.level;
231
232            if let Some(fmt) = next_fmt.as_ref()
233                && to_usize(fmt.start) == index
234            {
235                font = fmt.font;
236                dpem = fmt.dpem;
237                next_fmt = font_tokens.next();
238            }
239
240            let mut new_script = None;
241            if props.script().is_real() {
242                if first_real.is_none() && !c.is_control() {
243                    first_real = Some(c);
244                }
245                let script = script_to_fontique(props.script());
246                if input.script == UNKNOWN_SCRIPT {
247                    input.script = script;
248                } else if script != UNKNOWN_SCRIPT && script != input.script {
249                    new_script = Some(script);
250                }
251            }
252
253            if hard_break || control_break || bidi_break || new_script.is_some() {
254                let range = (start..non_control_end).into();
255                let special = match () {
256                    _ if hard_break => RunSpecial::HardBreak,
257                    _ if last_is_htab => RunSpecial::HTab,
258                    _ if last_is_control || is_break => RunSpecial::None,
259                    _ => RunSpecial::NoBreak,
260                };
261
262                self.push_run(font, input, range, breaks, special, first_real)?;
263                first_real = None;
264
265                start = index;
266                non_control_end = index;
267                input.level = levels[index];
268                input.script = UNKNOWN_SCRIPT;
269                breaks = Default::default();
270            } else if is_break && !is_control {
271                // We do break runs when hitting control chars, but only when
272                // encountering the next non-control character.
273                breaks.push(shaper::GlyphBreak::new(to_u32(index)));
274            }
275
276            last_is_control = is_control;
277            last_is_htab = is_htab;
278            input.dpem = dpem;
279            if let Some(script) = new_script {
280                input.script = script;
281            }
282        }
283
284        debug_assert!(analyzer.next().is_none());
285        let hard_break = last_props
286            .map(|props| matches!(props.line_break(), LB::BK | LB::CR | LB::LF | LB::NL))
287            .unwrap_or(false);
288
289        // Conclude: add last run. This may be empty, but we want it anyway.
290        if !last_is_control {
291            non_control_end = text.len();
292        }
293        let range = (start..non_control_end).into();
294        let special = match () {
295            _ if hard_break => RunSpecial::HardBreak,
296            _ if last_is_htab => RunSpecial::HTab,
297            _ => RunSpecial::None,
298        };
299
300        self.push_run(font, input, range, breaks, special, first_real)?;
301
302        // Following a hard break we have an implied empty line.
303        if hard_break {
304            let range = (text.len()..text.len()).into();
305            input.level = default_para_level.unwrap_or(LTR_LEVEL);
306            breaks = Default::default();
307            self.push_run(font, input, range, breaks, RunSpecial::None, None)?;
308        }
309
310        /*
311        println!("text: {}", text);
312        let fonts = fonts::library();
313        for run in &self.runs {
314            let slice = &text[run.range];
315            print!(
316                "\t{:?}, text[{}..{}]: '{}', ",
317                run.level, run.range.start, run.range.end, slice
318            );
319            match run.special {
320                RunSpecial::None => (),
321                RunSpecial::HardBreak => print!("HardBreak, "),
322                RunSpecial::NoBreak => print!("NoBreak, "),
323                RunSpecial::HTab => print!("HTab, "),
324            }
325            print!("breaks=[");
326            let mut iter = run.breaks.iter();
327            if let Some(b) = iter.next() {
328                print!("{}", b.index);
329            }
330            for b in iter {
331                print!(", {}", b.index);
332            }
333            print!("]");
334            if let Some(name) = fonts.get_face_store(run.face_id).name_full() {
335                print!(", {name}");
336            }
337            println!();
338        }
339        */
340        Ok(())
341    }
342}
343
344trait ScriptExt {
345    #[allow(clippy::wrong_self_convention)]
346    fn is_real(self) -> bool;
347}
348impl ScriptExt for swash::text::Script {
349    fn is_real(self) -> bool {
350        use swash::text::Script::*;
351        !matches!(self, Common | Unknown | Inherited)
352    }
353}
354
355pub(crate) const UNKNOWN_SCRIPT: fontique::Script = fontique::Script(*b"Zzzz");