use alloc::{string::String, vec::Vec};
pub use azul_core::{
app_resources::{
FontMetrics, GlyphIndex, IndexOfLineBreak, LayoutedGlyphs, LineBreaks, LineLength,
RemainingSpaceToRight, ShapedWord, ShapedWords, Word, WordIndex, WordPositions, WordType,
Words,
},
callbacks::InlineText,
display_list::GlyphInstance,
ui_solver::{
InlineTextLayout, ResolvedTextLayoutOptions, TextLayoutOptions, DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT, DEFAULT_TAB_WIDTH, DEFAULT_WORD_SPACING,
},
window::{LogicalPosition, LogicalRect, LogicalSize},
};
pub use azul_css::FontRef;
pub use super::shaping::ParsedFont;
pub fn parse_font(
font_bytes: &[u8],
font_index: usize,
parse_outlines: bool,
) -> Option<ParsedFont> {
ParsedFont::from_bytes(font_bytes, font_index, parse_outlines)
}
pub fn split_text_into_words(text: &str) -> Words {
use unicode_normalization::UnicodeNormalization;
let normalized_string = text.nfc().collect::<String>();
let normalized_chars = normalized_string.chars().collect::<Vec<char>>();
let mut words = Vec::new();
let mut current_word_start = 0;
let mut last_char_idx = 0;
let mut last_char_was_whitespace = false;
for (ch_idx, ch) in normalized_chars.iter().enumerate() {
let ch = *ch;
let current_char_is_whitespace = ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
let should_push_delimiter = match ch {
' ' => Some(Word {
start: last_char_idx + 1,
end: ch_idx + 1,
word_type: WordType::Space,
}),
'\t' => Some(Word {
start: last_char_idx + 1,
end: ch_idx + 1,
word_type: WordType::Tab,
}),
'\n' => {
Some(if normalized_chars[last_char_idx] == '\r' {
Word {
start: last_char_idx,
end: ch_idx + 1,
word_type: WordType::Return,
}
} else {
Word {
start: last_char_idx + 1,
end: ch_idx + 1,
word_type: WordType::Return,
}
})
}
_ => None,
};
let should_push_word = if current_char_is_whitespace && !last_char_was_whitespace {
Some(Word {
start: current_word_start,
end: ch_idx,
word_type: WordType::Word,
})
} else {
None
};
if current_char_is_whitespace {
current_word_start = ch_idx + 1;
}
let mut push_words = |arr: [Option<Word>; 2]| {
words.extend(arr.iter().filter_map(|e| *e));
};
push_words([should_push_word, should_push_delimiter]);
last_char_was_whitespace = current_char_is_whitespace;
last_char_idx = ch_idx;
}
if current_word_start != last_char_idx + 1 {
words.push(Word {
start: current_word_start,
end: normalized_chars.len(),
word_type: WordType::Word,
});
}
if let Some(Word {
word_type: WordType::Return,
..
}) = words.last()
{
words.pop();
}
Words {
items: words.into(),
internal_str: normalized_string.into(),
internal_chars: normalized_chars.iter().map(|c| *c as u32).collect(),
}
}
pub fn shape_words(words: &Words, font: &ParsedFont) -> ShapedWords {
let (script, lang) = super::shaping::estimate_script_and_language(&words.internal_str);
let space_advance = font
.get_space_width()
.unwrap_or(font.font_metrics.units_per_em as usize);
let mut longest_word_width = 0_usize;
let shaped_words = words
.items
.iter()
.filter(|w| w.word_type == WordType::Word)
.map(|word| {
use super::shaping::ShapedTextBufferUnsized;
let chars = &words.internal_chars.as_ref()[word.start..word.end];
let shaped_word = font.shape(chars, script, lang);
let word_width = shaped_word.get_word_visual_width_unscaled();
longest_word_width = longest_word_width.max(word_width);
let ShapedTextBufferUnsized { infos } = shaped_word;
ShapedWord {
glyph_infos: infos.into(),
word_width,
}
})
.collect();
ShapedWords {
items: shaped_words,
longest_word_width,
space_advance,
font_metrics_units_per_em: font.font_metrics.units_per_em,
font_metrics_ascender: font.font_metrics.get_ascender_unscaled(),
font_metrics_descender: font.font_metrics.get_descender_unscaled(),
font_metrics_line_gap: font.font_metrics.get_line_gap_unscaled(),
}
}
pub fn position_words(
words: &Words,
shaped_words: &ShapedWords,
text_layout_options: &ResolvedTextLayoutOptions,
) -> WordPositions {
use core::f32;
use azul_core::{app_resources::WordPosition, ui_solver::InlineTextLine};
use self::{LineCaretIntersection::*, WordType::*};
let font_size_px = text_layout_options.font_size_px;
let space_advance_px = shaped_words.get_space_advance_px(text_layout_options.font_size_px);
let word_spacing_px = space_advance_px
* text_layout_options
.word_spacing
.as_ref()
.copied()
.unwrap_or(DEFAULT_WORD_SPACING);
let line_height_px = space_advance_px
* text_layout_options
.line_height
.as_ref()
.copied()
.unwrap_or(DEFAULT_LINE_HEIGHT);
let tab_width_px = space_advance_px
* text_layout_options
.tab_width
.as_ref()
.copied()
.unwrap_or(DEFAULT_TAB_WIDTH);
let spacing_multiplier = text_layout_options
.letter_spacing
.as_ref()
.copied()
.unwrap_or(0.0);
let mut line_breaks = Vec::new();
let mut word_positions = Vec::new();
let mut line_caret_x = text_layout_options.leading.as_ref().copied().unwrap_or(0.0);
let mut line_caret_y = font_size_px + line_height_px;
let mut shaped_word_idx = 0;
let mut last_shaped_word_word_idx = 0;
let mut last_line_start_idx = 0;
let last_word_idx = words.items.len().saturating_sub(1);
for (word_idx, word) in words.items.iter().enumerate() {
match word.word_type {
Word => {
let shaped_word = match shaped_words.items.get(shaped_word_idx) {
Some(s) => s,
None => continue,
};
let letter_spacing_px =
spacing_multiplier * shaped_word.number_of_glyphs().saturating_sub(1) as f32;
let shaped_word_width = shaped_word.get_word_width(
shaped_words.font_metrics_units_per_em,
text_layout_options.font_size_px,
) + letter_spacing_px;
let caret_intersection = LineCaretIntersection::new(
line_caret_x,
shaped_word_width,
line_caret_y,
font_size_px + line_height_px,
text_layout_options.max_horizontal_width.as_ref().copied(),
);
match caret_intersection {
NoLineBreak { new_x, new_y } => {
word_positions.push(WordPosition {
shaped_word_index: Some(shaped_word_idx),
position: LogicalPosition::new(line_caret_x, line_caret_y),
size: LogicalSize::new(
shaped_word_width,
font_size_px + line_height_px,
),
});
line_caret_x = new_x;
line_caret_y = new_y;
}
LineBreak { new_x, new_y } => {
line_breaks.push(InlineTextLine {
word_start: last_line_start_idx,
word_end: word_idx.saturating_sub(1).max(last_line_start_idx),
bounds: LogicalRect::new(
LogicalPosition::new(0.0, line_caret_y),
LogicalSize::new(line_caret_x, font_size_px + line_height_px),
),
});
last_line_start_idx = word_idx;
word_positions.push(WordPosition {
shaped_word_index: Some(shaped_word_idx),
position: LogicalPosition::new(new_x, new_y),
size: LogicalSize::new(
shaped_word_width,
font_size_px + line_height_px,
),
});
line_caret_x = new_x + shaped_word_width; line_caret_y = new_y;
}
}
shaped_word_idx += 1;
last_shaped_word_word_idx = word_idx;
}
Return => {
if word_idx != last_word_idx {
line_breaks.push(InlineTextLine {
word_start: last_line_start_idx,
word_end: word_idx.saturating_sub(1).max(last_line_start_idx),
bounds: LogicalRect::new(
LogicalPosition::new(0.0, line_caret_y),
LogicalSize::new(line_caret_x, font_size_px + line_height_px),
),
});
last_line_start_idx = word_idx + 1;
}
word_positions.push(WordPosition {
shaped_word_index: None,
position: LogicalPosition::new(line_caret_x, line_caret_y),
size: LogicalSize::new(0.0, font_size_px + line_height_px),
});
if word_idx != last_word_idx {
line_caret_x = 0.0;
line_caret_y = line_caret_y + font_size_px + line_height_px;
}
}
Space | Tab => {
let x_advance = match word.word_type {
Space => word_spacing_px,
Tab => tab_width_px,
_ => word_spacing_px, };
let caret_intersection = LineCaretIntersection::new(
line_caret_x,
x_advance, line_caret_y,
font_size_px + line_height_px,
text_layout_options.max_horizontal_width.as_ref().copied(),
);
match caret_intersection {
NoLineBreak { new_x, new_y } => {
word_positions.push(WordPosition {
shaped_word_index: None,
position: LogicalPosition::new(line_caret_x, line_caret_y),
size: LogicalSize::new(x_advance, font_size_px + line_height_px),
});
line_caret_x = new_x;
line_caret_y = new_y;
}
LineBreak { new_x, new_y } => {
if word_idx != last_word_idx {
line_breaks.push(InlineTextLine {
word_start: last_line_start_idx,
word_end: word_idx.saturating_sub(1).max(last_line_start_idx),
bounds: LogicalRect::new(
LogicalPosition::new(0.0, line_caret_y),
LogicalSize::new(line_caret_x, font_size_px + line_height_px),
),
});
last_line_start_idx = word_idx;
}
word_positions.push(WordPosition {
shaped_word_index: None,
position: LogicalPosition::new(line_caret_x, line_caret_y),
size: LogicalSize::new(x_advance, font_size_px + line_height_px),
});
if word_idx != last_word_idx {
line_caret_x = new_x; line_caret_y = new_y;
}
}
}
}
}
}
line_breaks.push(InlineTextLine {
word_start: last_line_start_idx,
word_end: last_shaped_word_word_idx,
bounds: LogicalRect::new(
LogicalPosition::new(0.0, line_caret_y),
LogicalSize::new(line_caret_x, font_size_px + line_height_px),
),
});
let longest_line_width = line_breaks
.iter()
.map(|line| line.bounds.size.width)
.fold(0.0_f32, f32::max);
let content_size_y = line_breaks.len() as f32 * (font_size_px + line_height_px);
let content_size_x = text_layout_options
.max_horizontal_width
.as_ref()
.copied()
.unwrap_or(longest_line_width);
let content_size = LogicalSize::new(content_size_x, content_size_y);
WordPositions {
text_layout_options: text_layout_options.clone(),
trailing: line_caret_x,
number_of_shaped_words: shaped_word_idx,
number_of_lines: line_breaks.len(),
content_size,
word_positions,
line_breaks,
}
}
pub fn word_positions_to_inline_text_layout(word_positions: &WordPositions) -> InlineTextLayout {
InlineTextLayout {
lines: word_positions.line_breaks.clone().into(),
content_size: word_positions.content_size,
}
}
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
enum LineCaretIntersection {
NoLineBreak { new_x: f32, new_y: f32 },
LineBreak { new_x: f32, new_y: f32 },
}
impl LineCaretIntersection {
#[inline]
fn new(
current_x: f32,
word_width: f32,
current_y: f32,
line_height: f32,
max_width: Option<f32>,
) -> Self {
match max_width {
None => LineCaretIntersection::NoLineBreak {
new_x: current_x + word_width,
new_y: current_y,
},
Some(max) => {
if current_x == 0.0 && max < word_width {
LineCaretIntersection::NoLineBreak {
new_x: current_x + word_width,
new_y: current_y,
}
} else if (current_x + word_width) > max {
LineCaretIntersection::LineBreak {
new_x: 0.0,
new_y: current_y + line_height,
}
} else {
LineCaretIntersection::NoLineBreak {
new_x: current_x + word_width,
new_y: current_y,
}
}
}
}
}
}
pub fn shape_text(font: &FontRef, text: &str, options: &ResolvedTextLayoutOptions) -> InlineText {
let font_data = font.get_data();
let parsed_font_downcasted = unsafe { &*(font_data.parsed as *const ParsedFont) };
let words = split_text_into_words(text);
let shaped_words = shape_words(&words, parsed_font_downcasted);
let word_positions = position_words(&words, &shaped_words, options);
let inline_text_layout = word_positions_to_inline_text_layout(&word_positions);
azul_core::app_resources::get_inline_text(
&words,
&shaped_words,
&word_positions,
&inline_text_layout,
)
}