1#![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 HardBreak,
24 NoBreak,
26 HTab,
28}
29
30impl TextDisplay {
31 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 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 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 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 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 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 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 let hard_break = boundary == Boundary::Mandatory;
226 let is_break = hard_break || boundary == Boundary::Line;
228
229 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 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 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 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 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");