Current section
Files
Jump to
Current section
Files
native/emerge_skia/src/tree/render/text.rs
use super::box_model::content_insets;
use super::color::{DEFAULT_TEXT_COLOR, color_to_u32};
use crate::render_scene::{DrawPrimitive, RenderNode};
use crate::renderer::{make_font_with_style, measure_text_visual_metrics};
use crate::tree::attrs::{Attrs, TextAlign};
use crate::tree::element::{Frame, NodeRuntime};
use crate::tree::layout::{FontContext, font_info_with_inheritance};
use crate::tree::text_layout::{TextLayoutStyle, layout_text_lines};
pub(super) const TEXT_SELECTION_COLOR: u32 = 0x4A90E266;
#[derive(Clone, Copy)]
pub(super) struct TextRunStyle<'a> {
pub(super) font_size: f32,
pub(super) color: u32,
pub(super) family: &'a str,
pub(super) weight: u16,
pub(super) italic: bool,
pub(super) letter_spacing: f32,
pub(super) word_spacing: f32,
}
fn text_input_selected_range(
cursor: u32,
selection_anchor: Option<u32>,
content_len: u32,
) -> Option<(u32, u32)> {
let cursor = cursor.min(content_len);
let anchor = selection_anchor?.min(content_len);
(anchor != cursor).then_some((anchor.min(cursor), anchor.max(cursor)))
}
fn text_input_display_state(
content: &str,
cursor: u32,
selection_anchor: Option<u32>,
preedit: Option<&str>,
) -> (u32, u32, String) {
let content_len = content.chars().count() as u32;
let cursor = cursor.min(content_len);
let (replace_start, replace_end) = if preedit.is_some() {
text_input_selected_range(cursor, selection_anchor, content_len).unwrap_or((cursor, cursor))
} else {
(cursor, cursor)
};
let displayed_text = match preedit {
Some(preedit_text) => {
let prefix: String = content.chars().take(replace_start as usize).collect();
let suffix: String = content.chars().skip(replace_end as usize).collect();
let mut value = String::with_capacity(prefix.len() + preedit_text.len() + suffix.len());
value.push_str(&prefix);
value.push_str(preedit_text);
value.push_str(&suffix);
value
}
None => content.to_string(),
};
(replace_start, replace_end, displayed_text)
}
fn map_committed_to_displayed(
index: u32,
replace_start: u32,
replace_end: u32,
preedit_len: u32,
) -> u32 {
if preedit_len == 0 || index <= replace_start {
index
} else if index >= replace_end {
index - (replace_end - replace_start) + preedit_len
} else {
replace_start
}
}
pub(super) fn render_text_items(
frame: Frame,
attrs: &Attrs,
inherited: &FontContext,
) -> Vec<RenderNode> {
let Some(content) = attrs.content.as_deref() else {
return Vec::new();
};
// Use inherited font context for missing values
let font_size = attrs
.font_size
.map(|s| s as f32)
.or(inherited.font_size)
.unwrap_or(16.0);
let color = attrs
.font_color
.as_ref()
.map(color_to_u32)
.or(inherited.font_color)
.unwrap_or(DEFAULT_TEXT_COLOR);
let underline = attrs
.font_underline
.or(inherited.font_underline)
.unwrap_or(false);
let strike = attrs.font_strike.or(inherited.font_strike).unwrap_or(false);
let letter_spacing = attrs
.font_letter_spacing
.map(|v| v as f32)
.or(inherited.font_letter_spacing)
.unwrap_or(0.0);
let word_spacing = attrs
.font_word_spacing
.map(|v| v as f32)
.or(inherited.font_word_spacing)
.unwrap_or(0.0);
let (family, weight, italic) = font_info_with_inheritance(attrs, inherited);
let style = TextRunStyle {
font_size,
color,
family: &family,
weight,
italic,
letter_spacing,
word_spacing,
};
let insets = content_insets(attrs);
let inset_left = insets.left;
let inset_top = insets.top;
let inset_right = insets.right;
let (ascent, _) = text_metrics_with_font(font_size, &family, weight, italic);
let (text_left_overhang, text_width) = text_alignment_metrics(content, style);
let content_width = frame.width - inset_left - inset_right;
let text_align = attrs
.text_align
.or(inherited.text_align)
.unwrap_or_default();
let text_x = match text_align {
TextAlign::Left => frame.x + inset_left + text_left_overhang,
TextAlign::Center => {
frame.x + inset_left + (content_width - text_width) / 2.0 + text_left_overhang
}
TextAlign::Right => frame.x + frame.width - inset_right - text_width + text_left_overhang,
};
let baseline_y = frame.y + inset_top + ascent;
let mut items = text_run_items(text_x, baseline_y, content, style);
items.extend(text_decoration_items(TextDecorationSpec {
x: text_x,
baseline_y,
width: text_width,
font_size,
color,
underline,
strike,
}));
items
}
pub(super) fn render_text_input_items(
items: &mut Vec<RenderNode>,
frame: Frame,
attrs: &Attrs,
runtime: &NodeRuntime,
inherited: &FontContext,
) -> Option<(f32, f32, f32, f32)> {
let content = attrs.content.as_deref().unwrap_or("");
let preedit = runtime.text_input_preedit.as_deref();
let font_size = attrs
.font_size
.map(|s| s as f32)
.or(inherited.font_size)
.unwrap_or(16.0);
let color = attrs
.font_color
.as_ref()
.map(color_to_u32)
.or(inherited.font_color)
.unwrap_or(DEFAULT_TEXT_COLOR);
let underline = attrs
.font_underline
.or(inherited.font_underline)
.unwrap_or(false);
let strike = attrs.font_strike.or(inherited.font_strike).unwrap_or(false);
let letter_spacing = attrs
.font_letter_spacing
.map(|v| v as f32)
.or(inherited.font_letter_spacing)
.unwrap_or(0.0);
let word_spacing = attrs
.font_word_spacing
.map(|v| v as f32)
.or(inherited.font_word_spacing)
.unwrap_or(0.0);
let (family, weight, italic) = font_info_with_inheritance(attrs, inherited);
let style = TextRunStyle {
font_size,
color,
family: &family,
weight,
italic,
letter_spacing,
word_spacing,
};
let insets = content_insets(attrs);
let inset_left = insets.left;
let inset_top = insets.top;
let inset_right = insets.right;
let (ascent, descent) = text_metrics_with_font(font_size, &family, weight, italic);
let content_char_count = content.chars().count() as u32;
let cursor = runtime
.text_input_cursor
.unwrap_or(content_char_count)
.min(content_char_count);
let (replace_start, replace_end, displayed_text) = text_input_display_state(
content,
cursor,
runtime.text_input_selection_anchor,
preedit,
);
let (text_left_overhang, text_width) = text_alignment_metrics(&displayed_text, style);
let content_width = frame.width - inset_left - inset_right;
let text_align = attrs
.text_align
.or(inherited.text_align)
.unwrap_or_default();
let text_x = match text_align {
TextAlign::Left => frame.x + inset_left + text_left_overhang,
TextAlign::Center => {
frame.x + inset_left + (content_width - text_width) / 2.0 + text_left_overhang
}
TextAlign::Right => frame.x + frame.width - inset_right - text_width + text_left_overhang,
};
let baseline_y = frame.y + inset_top + ascent;
if let Some(anchor) = runtime.text_input_selection_anchor {
let anchor = anchor.min(content_char_count);
if anchor != cursor && !(preedit.is_some() && replace_end > replace_start) {
let preedit_len = preedit
.map(|value| value.chars().count() as u32)
.unwrap_or(0);
let sel_start = anchor.min(cursor);
let sel_end = anchor.max(cursor);
let displayed_start =
map_committed_to_displayed(sel_start, replace_start, replace_end, preedit_len);
let displayed_end =
map_committed_to_displayed(sel_end, replace_start, replace_end, preedit_len);
let start_offset =
text_offset_for_char_index(&displayed_text, displayed_start as usize, style);
let end_offset =
text_offset_for_char_index(&displayed_text, displayed_end as usize, style);
let selection_width = (end_offset - start_offset).max(0.0);
if selection_width > 0.0 {
let selection_top = baseline_y - ascent;
let selection_height = (ascent + descent).max(font_size * 0.9);
items.push(RenderNode::Primitive(DrawPrimitive::Rect(
text_x + start_offset,
selection_top,
selection_width,
selection_height,
TEXT_SELECTION_COLOR,
)));
}
}
}
items.extend(text_run_items(text_x, baseline_y, &displayed_text, style));
items.extend(text_decoration_items(TextDecorationSpec {
x: text_x,
baseline_y,
width: text_width,
font_size,
color,
underline,
strike,
}));
if let Some(preedit_text) = preedit {
let preedit_start_offset =
text_offset_for_char_index(&displayed_text, replace_start as usize, style);
let preedit_width = measure_text_width_with_font(
preedit_text,
style.font_size,
style.family,
style.weight,
style.italic,
style.letter_spacing,
style.word_spacing,
);
items.extend(text_decoration_items(TextDecorationSpec {
x: text_x + preedit_start_offset,
baseline_y,
width: preedit_width,
font_size,
color,
underline: true,
strike: false,
}));
}
if runtime.text_input_focused {
let displayed_char_count = displayed_text.chars().count() as u32;
let caret_char_index = if let Some(preedit_text) = preedit {
let preedit_len = preedit_text.chars().count() as u32;
let preedit_cursor_end = runtime
.text_input_preedit_cursor
.map(|(_start, end)| end.min(preedit_len))
.unwrap_or(preedit_len);
(replace_start + preedit_cursor_end).min(displayed_char_count)
} else {
cursor.min(displayed_char_count)
};
let caret_offset =
text_offset_for_char_index(&displayed_text, caret_char_index as usize, style);
let caret_x = text_x + caret_offset;
let caret_top = baseline_y - ascent;
let caret_height = (ascent + descent).max(font_size * 0.9);
let caret_width = (font_size * 0.08).max(1.0);
items.push(RenderNode::Primitive(DrawPrimitive::Rect(
caret_x,
caret_top,
caret_width,
caret_height,
color,
)));
return Some((caret_x, caret_top, caret_width, caret_height));
}
None
}
pub(super) fn render_multiline_text_input_items(
items: &mut Vec<RenderNode>,
frame: Frame,
attrs: &Attrs,
runtime: &NodeRuntime,
inherited: &FontContext,
) -> Option<(f32, f32, f32, f32)> {
let content = attrs.content.as_deref().unwrap_or("");
let preedit = runtime.text_input_preedit.as_deref();
let font_size = attrs
.font_size
.map(|s| s as f32)
.or(inherited.font_size)
.unwrap_or(16.0);
let color = attrs
.font_color
.as_ref()
.map(color_to_u32)
.or(inherited.font_color)
.unwrap_or(DEFAULT_TEXT_COLOR);
let underline = attrs
.font_underline
.or(inherited.font_underline)
.unwrap_or(false);
let strike = attrs.font_strike.or(inherited.font_strike).unwrap_or(false);
let letter_spacing = attrs
.font_letter_spacing
.map(|v| v as f32)
.or(inherited.font_letter_spacing)
.unwrap_or(0.0);
let word_spacing = attrs
.font_word_spacing
.map(|v| v as f32)
.or(inherited.font_word_spacing)
.unwrap_or(0.0);
let (family, weight, italic) = font_info_with_inheritance(attrs, inherited);
let style = TextRunStyle {
font_size,
color,
family: &family,
weight,
italic,
letter_spacing,
word_spacing,
};
let insets = content_insets(attrs);
let inset_left = insets.left;
let inset_top = insets.top;
let inset_right = insets.right;
let content_width = (frame.width - inset_left - inset_right).max(0.0);
let text_align = attrs
.text_align
.or(inherited.text_align)
.unwrap_or_default();
let (ascent, descent) = text_metrics_with_font(font_size, &family, weight, italic);
let font = make_font_with_style(&family, weight, italic, font_size);
let content_char_count = content.chars().count() as u32;
let cursor = runtime
.text_input_cursor
.unwrap_or(content_char_count)
.min(content_char_count);
let (replace_start, replace_end, displayed_text) = text_input_display_state(
content,
cursor,
runtime.text_input_selection_anchor,
preedit,
);
let layout = layout_text_lines(
&displayed_text,
Some(content_width),
(ascent, descent),
TextLayoutStyle {
font_size,
letter_spacing,
word_spacing,
},
|ch| {
let (glyph_width, _bounds) = font.measure_str(ch.to_string(), None);
glyph_width
},
);
let line_x = |line_width: f32| match text_align {
TextAlign::Left => frame.x + inset_left,
TextAlign::Center => frame.x + inset_left + (content_width - line_width) / 2.0,
TextAlign::Right => frame.x + frame.width - inset_right - line_width,
};
let line_top = |line_index: usize| frame.y + inset_top + line_index as f32 * layout.line_height;
let preedit_len = preedit
.map(|value| value.chars().count() as u32)
.unwrap_or(0);
if let Some(anchor) = runtime.text_input_selection_anchor {
let anchor = anchor.min(content_char_count);
if anchor != cursor && !(preedit.is_some() && replace_end > replace_start) {
let displayed_start = map_committed_to_displayed(
anchor.min(cursor),
replace_start,
replace_end,
preedit_len,
) as usize;
let displayed_end = map_committed_to_displayed(
anchor.max(cursor),
replace_start,
replace_end,
preedit_len,
) as usize;
for (line_index, line) in layout.lines.iter().enumerate() {
let line_start = displayed_start.max(line.start).min(line.visual_end);
let line_end = displayed_end.max(line.start).min(line.visual_end);
if line_end <= line_start {
continue;
}
let x = line_x(line.width) + line.offset_for_cursor(line_start);
let width = line.offset_for_cursor(line_end) - line.offset_for_cursor(line_start);
if width <= 0.0 {
continue;
}
items.push(RenderNode::Primitive(DrawPrimitive::Rect(
x,
line_top(line_index),
width,
layout.line_height,
TEXT_SELECTION_COLOR,
)));
}
}
}
for (line_index, line) in layout.lines.iter().enumerate() {
let x = line_x(line.width);
let baseline_y = line_top(line_index) + layout.ascent;
items.extend(text_run_items(x, baseline_y, &line.text, style));
items.extend(text_decoration_items(TextDecorationSpec {
x,
baseline_y,
width: line.width,
font_size,
color,
underline,
strike,
}));
}
if let Some(preedit_text) = preedit {
let preedit_start = replace_start as usize;
let preedit_end = preedit_start + preedit_text.chars().count();
for (line_index, line) in layout.lines.iter().enumerate() {
let line_start = preedit_start.max(line.start).min(line.visual_end);
let line_end = preedit_end.max(line.start).min(line.visual_end);
if line_end <= line_start {
continue;
}
let baseline_y = line_top(line_index) + layout.ascent;
let x = line_x(line.width) + line.offset_for_cursor(line_start);
let width = line.offset_for_cursor(line_end) - line.offset_for_cursor(line_start);
items.extend(text_decoration_items(TextDecorationSpec {
x,
baseline_y,
width,
font_size,
color,
underline: true,
strike: false,
}));
}
}
if runtime.text_input_focused {
let displayed_char_count = displayed_text.chars().count() as u32;
let caret_char_index = if let Some(preedit_text) = preedit {
let preedit_len = preedit_text.chars().count() as u32;
let preedit_cursor_end = runtime
.text_input_preedit_cursor
.map(|(_start, end)| end.min(preedit_len))
.unwrap_or(preedit_len);
(replace_start + preedit_cursor_end).min(displayed_char_count)
} else {
cursor.min(displayed_char_count)
} as usize;
let line_index = layout.line_index_for_cursor(caret_char_index);
let line = &layout.lines[line_index];
let caret_x = line_x(line.width) + line.offset_for_cursor(caret_char_index);
let caret_top = line_top(line_index);
let caret_height = layout.line_height.max(font_size * 0.9);
let caret_width = (font_size * 0.08).max(1.0);
items.push(RenderNode::Primitive(DrawPrimitive::Rect(
caret_x,
caret_top,
caret_width,
caret_height,
color,
)));
return Some((caret_x, caret_top, caret_width, caret_height));
}
None
}
pub(super) fn text_run_items(
x: f32,
baseline_y: f32,
text: &str,
style: TextRunStyle<'_>,
) -> Vec<RenderNode> {
if text.is_empty() {
return Vec::new();
}
let mut items = Vec::new();
if style.letter_spacing == 0.0 && style.word_spacing == 0.0 {
items.push(RenderNode::Primitive(DrawPrimitive::TextWithFont(
x,
baseline_y,
text.to_string(),
style.font_size,
style.color,
style.family.to_string(),
style.weight,
style.italic,
)));
return items;
}
let measure_font =
make_font_with_style(style.family, style.weight, style.italic, style.font_size);
let mut cursor_x = x;
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
let glyph = ch.to_string();
items.push(RenderNode::Primitive(DrawPrimitive::TextWithFont(
cursor_x,
baseline_y,
glyph.clone(),
style.font_size,
style.color,
style.family.to_string(),
style.weight,
style.italic,
)));
let (glyph_width, _bounds) = measure_font.measure_str(&glyph, None);
cursor_x += glyph_width;
if chars.peek().is_some() {
cursor_x += style.letter_spacing;
if ch.is_whitespace() {
cursor_x += style.word_spacing;
}
}
}
items
}
pub(super) fn text_decoration_items(spec: TextDecorationSpec) -> Vec<RenderNode> {
let TextDecorationSpec {
x,
baseline_y,
width,
font_size,
color,
underline,
strike,
} = spec;
if width <= 0.0 || (!underline && !strike) {
return Vec::new();
}
let thickness = (font_size * 0.06).max(1.0);
let mut items = Vec::new();
if underline {
let y = baseline_y + font_size * 0.08 - thickness / 2.0;
items.push(RenderNode::Primitive(DrawPrimitive::Rect(
x, y, width, thickness, color,
)));
}
if strike {
let y = baseline_y - font_size * 0.3 - thickness / 2.0;
items.push(RenderNode::Primitive(DrawPrimitive::Rect(
x, y, width, thickness, color,
)));
}
items
}
pub(super) fn text_metrics_with_font(
font_size: f32,
family: &str,
weight: u16,
italic: bool,
) -> (f32, f32) {
let font = make_font_with_style(family, weight, italic, font_size);
let (_, metrics) = font.metrics();
(metrics.ascent.abs(), metrics.descent)
}
#[derive(Clone, Copy, Debug)]
pub(super) struct TextDecorationSpec {
pub(super) x: f32,
pub(super) baseline_y: f32,
pub(super) width: f32,
pub(super) font_size: f32,
pub(super) color: u32,
pub(super) underline: bool,
pub(super) strike: bool,
}
pub(super) fn measure_text_width_with_font(
text: &str,
font_size: f32,
family: &str,
weight: u16,
italic: bool,
letter_spacing: f32,
word_spacing: f32,
) -> f32 {
let font = make_font_with_style(family, weight, italic, font_size);
if text.is_empty() {
return 0.0;
}
if letter_spacing == 0.0 && word_spacing == 0.0 {
return measure_text_visual_metrics_with_style(text, family, weight, italic, font_size).1;
}
let mut total = 0.0;
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
let glyph = ch.to_string();
let (glyph_width, _bounds) = font.measure_str(&glyph, None);
total += glyph_width;
if chars.peek().is_some() {
total += letter_spacing;
if ch.is_whitespace() {
total += word_spacing;
}
}
}
total
}
fn text_alignment_metrics(text: &str, style: TextRunStyle<'_>) -> (f32, f32) {
if text.is_empty() {
return (0.0, 0.0);
}
if style.letter_spacing == 0.0 && style.word_spacing == 0.0 {
return measure_text_visual_metrics_with_style(
text,
style.family,
style.weight,
style.italic,
style.font_size,
);
}
(
0.0,
measure_text_width_with_font(
text,
style.font_size,
style.family,
style.weight,
style.italic,
style.letter_spacing,
style.word_spacing,
),
)
}
fn measure_text_visual_metrics_with_style(
text: &str,
family: &str,
weight: u16,
italic: bool,
font_size: f32,
) -> (f32, f32) {
let metrics = measure_text_visual_metrics(family, weight, italic, font_size, text);
(metrics.left_overhang, metrics.visual_width)
}
pub(super) fn text_offset_for_char_index(
text: &str,
char_index: usize,
style: TextRunStyle<'_>,
) -> f32 {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() {
return 0.0;
}
let clamped_index = char_index.min(chars.len());
if clamped_index == 0 {
return 0.0;
}
let font = make_font_with_style(style.family, style.weight, style.italic, style.font_size);
let mut total = 0.0;
for (idx, ch) in chars.iter().enumerate() {
if idx >= clamped_index {
break;
}
let glyph = ch.to_string();
let (glyph_width, _bounds) = font.measure_str(&glyph, None);
total += glyph_width;
if idx + 1 < chars.len() {
total += style.letter_spacing;
if ch.is_whitespace() {
total += style.word_spacing;
}
}
}
total
}