1use alloc::{string::String, vec::Vec};
5
6pub use azul_core::{
7 app_resources::{
8 FontMetrics, GlyphIndex, IndexOfLineBreak, LayoutedGlyphs, LineBreaks, LineLength,
9 RemainingSpaceToRight, ShapedWord, ShapedWords, Word, WordIndex, WordPositions, WordType,
10 Words,
11 },
12 callbacks::InlineText,
13 display_list::GlyphInstance,
14 ui_solver::{
15 InlineTextLayout, ResolvedTextLayoutOptions, TextLayoutOptions, DEFAULT_LETTER_SPACING,
16 DEFAULT_LINE_HEIGHT, DEFAULT_TAB_WIDTH, DEFAULT_WORD_SPACING,
17 },
18 window::{LogicalPosition, LogicalRect, LogicalSize},
19};
20pub use azul_css::FontRef;
21
22pub use super::shaping::ParsedFont;
23
24pub fn parse_font(
28 font_bytes: &[u8],
29 font_index: usize,
30 parse_outlines: bool,
31) -> Option<ParsedFont> {
32 ParsedFont::from_bytes(font_bytes, font_index, parse_outlines)
33}
34
35pub fn split_text_into_words(text: &str) -> Words {
37 use unicode_normalization::UnicodeNormalization;
38
39 let normalized_string = text.nfc().collect::<String>();
42 let normalized_chars = normalized_string.chars().collect::<Vec<char>>();
43
44 let mut words = Vec::new();
45
46 let mut current_word_start = 0;
50 let mut last_char_idx = 0;
51 let mut last_char_was_whitespace = false;
52
53 for (ch_idx, ch) in normalized_chars.iter().enumerate() {
54 let ch = *ch;
55 let current_char_is_whitespace = ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
56
57 let should_push_delimiter = match ch {
58 ' ' => Some(Word {
59 start: last_char_idx + 1,
60 end: ch_idx + 1,
61 word_type: WordType::Space,
62 }),
63 '\t' => Some(Word {
64 start: last_char_idx + 1,
65 end: ch_idx + 1,
66 word_type: WordType::Tab,
67 }),
68 '\n' => {
69 Some(if normalized_chars[last_char_idx] == '\r' {
70 Word {
72 start: last_char_idx,
73 end: ch_idx + 1,
74 word_type: WordType::Return,
75 }
76 } else {
77 Word {
79 start: last_char_idx + 1,
80 end: ch_idx + 1,
81 word_type: WordType::Return,
82 }
83 })
84 }
85 _ => None,
86 };
87
88 let should_push_word = if current_char_is_whitespace && !last_char_was_whitespace {
91 Some(Word {
92 start: current_word_start,
93 end: ch_idx,
94 word_type: WordType::Word,
95 })
96 } else {
97 None
98 };
99
100 if current_char_is_whitespace {
101 current_word_start = ch_idx + 1;
102 }
103
104 let mut push_words = |arr: [Option<Word>; 2]| {
105 words.extend(arr.iter().filter_map(|e| *e));
106 };
107
108 push_words([should_push_word, should_push_delimiter]);
109
110 last_char_was_whitespace = current_char_is_whitespace;
111 last_char_idx = ch_idx;
112 }
113
114 if current_word_start != last_char_idx + 1 {
116 words.push(Word {
117 start: current_word_start,
118 end: normalized_chars.len(),
119 word_type: WordType::Word,
120 });
121 }
122
123 if let Some(Word {
125 word_type: WordType::Return,
126 ..
127 }) = words.last()
128 {
129 words.pop();
130 }
131
132 Words {
133 items: words.into(),
134 internal_str: normalized_string.into(),
135 internal_chars: normalized_chars.iter().map(|c| *c as u32).collect(),
136 }
137}
138
139pub fn shape_words(words: &Words, font: &ParsedFont) -> ShapedWords {
142 let (script, lang) = super::shaping::estimate_script_and_language(&words.internal_str);
143
144 let space_advance = font
146 .get_space_width()
147 .unwrap_or(font.font_metrics.units_per_em as usize);
148
149 let mut longest_word_width = 0_usize;
150
151 let shaped_words = words
153 .items
154 .iter()
155 .filter(|w| w.word_type == WordType::Word)
156 .map(|word| {
157 use super::shaping::ShapedTextBufferUnsized;
158
159 let chars = &words.internal_chars.as_ref()[word.start..word.end];
160 let shaped_word = font.shape(chars, script, lang);
161 let word_width = shaped_word.get_word_visual_width_unscaled();
162
163 longest_word_width = longest_word_width.max(word_width);
164
165 let ShapedTextBufferUnsized { infos } = shaped_word;
166
167 ShapedWord {
168 glyph_infos: infos.into(),
169 word_width,
170 }
171 })
172 .collect();
173
174 ShapedWords {
175 items: shaped_words,
176 longest_word_width,
177 space_advance,
178 font_metrics_units_per_em: font.font_metrics.units_per_em,
179 font_metrics_ascender: font.font_metrics.get_ascender_unscaled(),
180 font_metrics_descender: font.font_metrics.get_descender_unscaled(),
181 font_metrics_line_gap: font.font_metrics.get_line_gap_unscaled(),
182 }
183}
184
185pub fn position_words(
188 words: &Words,
189 shaped_words: &ShapedWords,
190 text_layout_options: &ResolvedTextLayoutOptions,
191) -> WordPositions {
192 use core::f32;
193
194 use azul_core::{app_resources::WordPosition, ui_solver::InlineTextLine};
195
196 use self::{LineCaretIntersection::*, WordType::*};
197
198 let font_size_px = text_layout_options.font_size_px;
199 let space_advance_px = shaped_words.get_space_advance_px(text_layout_options.font_size_px);
200 let word_spacing_px = space_advance_px
201 * text_layout_options
202 .word_spacing
203 .as_ref()
204 .copied()
205 .unwrap_or(DEFAULT_WORD_SPACING);
206 let line_height_px = space_advance_px
207 * text_layout_options
208 .line_height
209 .as_ref()
210 .copied()
211 .unwrap_or(DEFAULT_LINE_HEIGHT);
212 let tab_width_px = space_advance_px
213 * text_layout_options
214 .tab_width
215 .as_ref()
216 .copied()
217 .unwrap_or(DEFAULT_TAB_WIDTH);
218 let spacing_multiplier = text_layout_options
219 .letter_spacing
220 .as_ref()
221 .copied()
222 .unwrap_or(0.0);
223
224 let mut line_breaks = Vec::new();
225 let mut word_positions = Vec::new();
226 let mut line_caret_x = text_layout_options.leading.as_ref().copied().unwrap_or(0.0);
227 let mut line_caret_y = font_size_px + line_height_px;
228 let mut shaped_word_idx = 0;
229 let mut last_shaped_word_word_idx = 0;
230 let mut last_line_start_idx = 0;
231
232 let last_word_idx = words.items.len().saturating_sub(1);
233
234 for (word_idx, word) in words.items.iter().enumerate() {
236 match word.word_type {
237 Word => {
238 let shaped_word = match shaped_words.items.get(shaped_word_idx) {
241 Some(s) => s,
242 None => continue,
243 };
244
245 let letter_spacing_px =
246 spacing_multiplier * shaped_word.number_of_glyphs().saturating_sub(1) as f32;
247
248 let shaped_word_width = shaped_word.get_word_width(
250 shaped_words.font_metrics_units_per_em,
251 text_layout_options.font_size_px,
252 ) + letter_spacing_px;
253
254 let caret_intersection = LineCaretIntersection::new(
256 line_caret_x,
257 shaped_word_width,
258 line_caret_y,
259 font_size_px + line_height_px,
260 text_layout_options.max_horizontal_width.as_ref().copied(),
261 );
262
263 match caret_intersection {
265 NoLineBreak { new_x, new_y } => {
266 word_positions.push(WordPosition {
267 shaped_word_index: Some(shaped_word_idx),
268 position: LogicalPosition::new(line_caret_x, line_caret_y),
269 size: LogicalSize::new(
270 shaped_word_width,
271 font_size_px + line_height_px,
272 ),
273 });
274 line_caret_x = new_x;
275 line_caret_y = new_y;
276 }
277 LineBreak { new_x, new_y } => {
278 line_breaks.push(InlineTextLine {
280 word_start: last_line_start_idx,
281 word_end: word_idx.saturating_sub(1).max(last_line_start_idx),
282 bounds: LogicalRect::new(
283 LogicalPosition::new(0.0, line_caret_y),
284 LogicalSize::new(line_caret_x, font_size_px + line_height_px),
285 ),
286 });
287 last_line_start_idx = word_idx;
288
289 word_positions.push(WordPosition {
290 shaped_word_index: Some(shaped_word_idx),
291 position: LogicalPosition::new(new_x, new_y),
292 size: LogicalSize::new(
293 shaped_word_width,
294 font_size_px + line_height_px,
295 ),
296 });
297 line_caret_x = new_x + shaped_word_width; line_caret_y = new_y;
299 }
300 }
301
302 shaped_word_idx += 1;
303 last_shaped_word_word_idx = word_idx;
304 }
305 Return => {
306 if word_idx != last_word_idx {
307 line_breaks.push(InlineTextLine {
308 word_start: last_line_start_idx,
309 word_end: word_idx.saturating_sub(1).max(last_line_start_idx),
310 bounds: LogicalRect::new(
311 LogicalPosition::new(0.0, line_caret_y),
312 LogicalSize::new(line_caret_x, font_size_px + line_height_px),
313 ),
314 });
315 last_line_start_idx = word_idx + 1;
317 }
318 word_positions.push(WordPosition {
319 shaped_word_index: None,
320 position: LogicalPosition::new(line_caret_x, line_caret_y),
321 size: LogicalSize::new(0.0, font_size_px + line_height_px),
322 });
323 if word_idx != last_word_idx {
324 line_caret_x = 0.0;
325 line_caret_y = line_caret_y + font_size_px + line_height_px;
326 }
327 }
328 Space | Tab => {
329 let x_advance = match word.word_type {
330 Space => word_spacing_px,
331 Tab => tab_width_px,
332 _ => word_spacing_px, };
334
335 let caret_intersection = LineCaretIntersection::new(
336 line_caret_x,
337 x_advance, line_caret_y,
339 font_size_px + line_height_px,
340 text_layout_options.max_horizontal_width.as_ref().copied(),
341 );
342
343 match caret_intersection {
344 NoLineBreak { new_x, new_y } => {
345 word_positions.push(WordPosition {
346 shaped_word_index: None,
347 position: LogicalPosition::new(line_caret_x, line_caret_y),
348 size: LogicalSize::new(x_advance, font_size_px + line_height_px),
349 });
350 line_caret_x = new_x;
351 line_caret_y = new_y;
352 }
353 LineBreak { new_x, new_y } => {
354 if word_idx != last_word_idx {
356 line_breaks.push(InlineTextLine {
357 word_start: last_line_start_idx,
358 word_end: word_idx.saturating_sub(1).max(last_line_start_idx),
359 bounds: LogicalRect::new(
360 LogicalPosition::new(0.0, line_caret_y),
361 LogicalSize::new(line_caret_x, font_size_px + line_height_px),
362 ),
363 });
364 last_line_start_idx = word_idx;
365 }
366 word_positions.push(WordPosition {
367 shaped_word_index: None,
368 position: LogicalPosition::new(line_caret_x, line_caret_y),
369 size: LogicalSize::new(x_advance, font_size_px + line_height_px),
370 });
371 if word_idx != last_word_idx {
372 line_caret_x = new_x; line_caret_y = new_y;
374 }
375 }
376 }
377 }
378 }
379 }
380
381 line_breaks.push(InlineTextLine {
382 word_start: last_line_start_idx,
383 word_end: last_shaped_word_word_idx,
384 bounds: LogicalRect::new(
385 LogicalPosition::new(0.0, line_caret_y),
386 LogicalSize::new(line_caret_x, font_size_px + line_height_px),
387 ),
388 });
389
390 let longest_line_width = line_breaks
391 .iter()
392 .map(|line| line.bounds.size.width)
393 .fold(0.0_f32, f32::max);
394
395 let content_size_y = line_breaks.len() as f32 * (font_size_px + line_height_px);
396 let content_size_x = text_layout_options
397 .max_horizontal_width
398 .as_ref()
399 .copied()
400 .unwrap_or(longest_line_width);
401 let content_size = LogicalSize::new(content_size_x, content_size_y);
402
403 WordPositions {
404 text_layout_options: text_layout_options.clone(),
405 trailing: line_caret_x,
406 number_of_shaped_words: shaped_word_idx,
407 number_of_lines: line_breaks.len(),
408 content_size,
409 word_positions,
410 line_breaks,
411 }
412}
413
414pub fn word_positions_to_inline_text_layout(word_positions: &WordPositions) -> InlineTextLayout {
416 InlineTextLayout {
417 lines: word_positions.line_breaks.clone().into(),
418 content_size: word_positions.content_size,
419 }
420}
421
422#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
423enum LineCaretIntersection {
424 NoLineBreak { new_x: f32, new_y: f32 },
427 LineBreak { new_x: f32, new_y: f32 },
430}
431
432impl LineCaretIntersection {
433 #[inline]
434 fn new(
435 current_x: f32,
436 word_width: f32,
437 current_y: f32,
438 line_height: f32,
439 max_width: Option<f32>,
440 ) -> Self {
441 match max_width {
442 None => LineCaretIntersection::NoLineBreak {
443 new_x: current_x + word_width,
444 new_y: current_y,
445 },
446 Some(max) => {
447 if current_x == 0.0 && max < word_width {
449 LineCaretIntersection::NoLineBreak {
450 new_x: current_x + word_width,
451 new_y: current_y,
452 }
453 } else if (current_x + word_width) > max {
454 LineCaretIntersection::LineBreak {
455 new_x: 0.0,
456 new_y: current_y + line_height,
457 }
458 } else {
459 LineCaretIntersection::NoLineBreak {
460 new_x: current_x + word_width,
461 new_y: current_y,
462 }
463 }
464 }
465 }
466 }
467}
468
469pub fn shape_text(font: &FontRef, text: &str, options: &ResolvedTextLayoutOptions) -> InlineText {
470 let font_data = font.get_data();
471 let parsed_font_downcasted = unsafe { &*(font_data.parsed as *const ParsedFont) };
472
473 let words = split_text_into_words(text);
474 let shaped_words = shape_words(&words, parsed_font_downcasted);
475 let word_positions = position_words(&words, &shaped_words, options);
476 let inline_text_layout = word_positions_to_inline_text_layout(&word_positions);
477
478 azul_core::app_resources::get_inline_text(
479 &words,
480 &shaped_words,
481 &word_positions,
482 &inline_text_layout,
483 )
484}