use alloc::vec::Vec;
use core::fmt;
use crate::widget::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct FontId(pub u16);
impl FontId {
pub const DEFAULT: Self = Self(0);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GlyphInfo {
pub advance_fp16: u16,
pub bearing_x: i16,
pub bearing_y: i16,
pub width: u16,
pub height: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FontLineMetrics {
pub line_height: u16,
pub ascent: i16,
pub descent: i16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GlyphPlacement {
pub ch: char,
pub info: GlyphInfo,
pub x: i32,
pub y: i32,
}
impl GlyphPlacement {
pub fn extent(&self) -> Rect {
Rect {
x: self.x + self.info.bearing_x as i32,
y: self.y - self.info.bearing_y as i32,
width: self.info.width as i32,
height: self.info.height as i32,
}
}
}
#[derive(Clone)]
pub struct ShapedText<'a> {
pub glyphs: Vec<GlyphPlacement>,
pub total_advance_fp16: i32,
pub bounds: Rect,
pub bidi_level: u8,
pub font: Option<&'a dyn FontMetrics>,
}
impl fmt::Debug for ShapedText<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ShapedText")
.field("glyphs", &self.glyphs)
.field("total_advance_fp16", &self.total_advance_fp16)
.field("bounds", &self.bounds)
.field("bidi_level", &self.bidi_level)
.field("has_font", &self.font.is_some())
.finish()
}
}
impl PartialEq for ShapedText<'_> {
fn eq(&self, other: &Self) -> bool {
self.glyphs == other.glyphs
&& self.total_advance_fp16 == other.total_advance_fp16
&& self.bounds == other.bounds
&& self.bidi_level == other.bidi_level
}
}
impl Eq for ShapedText<'_> {}
impl<'a> ShapedText<'a> {
pub fn empty(origin: (i32, i32)) -> Self {
Self {
glyphs: Vec::new(),
total_advance_fp16: 0,
bounds: Rect {
x: origin.0,
y: origin.1,
width: 0,
height: 0,
},
bidi_level: 0,
font: None,
}
}
}
pub trait FontMetrics {
fn glyph_metrics(&self, ch: char) -> Option<GlyphInfo>;
fn line_metrics(&self) -> FontLineMetrics;
fn glyph_coverage_row(
&self,
_ch: char,
_row: u16,
_x_offset: u16,
_coverage: &mut [u8],
) -> bool {
false
}
fn measure_fp16(&self, text: &str) -> i32 {
measure_text_fp16(self, text, 0)
}
fn shape(&self, text: &str, origin: (i32, i32)) -> ShapedText<'_>
where
Self: Sized,
{
shape_text_ltr(self, text, origin, 0)
}
}
#[derive(Clone, Copy, Default)]
pub struct WidgetFont(Option<&'static dyn FontMetrics>);
impl WidgetFont {
pub const fn new() -> Self {
Self(None)
}
pub const fn with_font(font: &'static dyn FontMetrics) -> Self {
Self(Some(font))
}
pub fn set(&mut self, font: &'static dyn FontMetrics) {
self.0 = Some(font);
}
pub fn clear(&mut self) {
self.0 = None;
}
pub fn is_set(&self) -> bool {
self.0.is_some()
}
pub fn resolve(&self) -> &'static dyn FontMetrics {
match self.0 {
Some(font) => font,
None => &crate::bitmap_font::FONT_6X10,
}
}
}
#[derive(Clone, Copy)]
pub struct FontRegistry<'a> {
entries: &'a [(FontId, &'static dyn FontMetrics)],
}
impl<'a> FontRegistry<'a> {
pub const fn new(entries: &'a [(FontId, &'static dyn FontMetrics)]) -> Self {
Self { entries }
}
pub fn resolve(&self, id: FontId) -> Option<&'static dyn FontMetrics> {
if id == FontId::DEFAULT {
return None;
}
self.entries
.iter()
.find(|(fid, _)| *fid == id)
.map(|(_, handle)| *handle)
}
}
impl FontRegistry<'static> {
pub const EMPTY: Self = Self { entries: &[] };
}
pub fn apply_font_registry(root: &crate::object::ObjectNode, registry: &FontRegistry<'_>) {
crate::style_cascade::resolve_tree_with_text(root, &mut |node, _style, text| {
let Some(handle) = registry.resolve(text.font_id) else {
return;
};
let widget = node.widget();
let mut w = widget.borrow_mut();
if let Some(slot) = w.widget_font_mut() {
slot.set(handle);
}
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WrappedLine {
pub start: usize,
pub end: usize,
pub advance_fp16: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WrappedText {
pub lines: Vec<WrappedLine>,
pub used_height: i32,
}
pub fn measure_text_fp16<F: FontMetrics + ?Sized>(
font: &F,
text: &str,
letter_spacing_px: i8,
) -> i32 {
let mut total = 0i32;
let mut glyph_count = 0u32;
let spacing = letter_spacing_px as i32 * 16;
for ch in text.chars() {
if is_zero_width_break(ch) {
continue;
}
if glyph_count > 0 {
total += spacing;
}
total += glyph_advance_fp16(font, ch);
glyph_count += 1;
}
total.max(0)
}
pub fn shape_text_ltr<'a>(
font: &'a dyn FontMetrics,
text: &str,
origin: (i32, i32),
letter_spacing_px: i8,
) -> ShapedText<'a> {
let mut shaped = ShapedText::empty(origin);
shaped.font = Some(font);
let mut cursor_fp16 = 0i32;
let mut has_bounds = false;
let mut glyph_count = 0u32;
let spacing = letter_spacing_px as i32 * 16;
for ch in text.chars() {
if is_zero_width_break(ch) {
continue;
}
if glyph_count > 0 {
cursor_fp16 += spacing;
}
let Some(info) = font.glyph_metrics(ch) else {
cursor_fp16 += fallback_advance_fp16(font);
glyph_count += 1;
continue;
};
let placement = GlyphPlacement {
ch,
info,
x: origin.0 + ((cursor_fp16 + 8) >> 4),
y: origin.1,
};
let extent = placement.extent();
shaped.bounds = if has_bounds {
shaped.bounds.union(extent)
} else {
has_bounds = true;
extent
};
shaped.glyphs.push(placement);
cursor_fp16 += info.advance_fp16 as i32;
glyph_count += 1;
}
shaped.total_advance_fp16 = cursor_fp16.max(0);
if !has_bounds {
shaped.bounds = Rect {
x: origin.0,
y: origin.1,
width: 0,
height: 0,
};
}
shaped
}
pub fn wrap_greedy_ltr<F: FontMetrics + ?Sized>(
font: &F,
text: &str,
max_width_px: i32,
letter_spacing_px: i8,
line_spacing_px: i8,
) -> WrappedText {
let mut lines = Vec::new();
let mut paragraph_start = 0usize;
for (idx, ch) in text.char_indices() {
if ch == '\n' {
wrap_span(
font,
text,
paragraph_start,
idx,
max_width_px,
letter_spacing_px,
&mut lines,
);
paragraph_start = idx + ch.len_utf8();
}
}
wrap_span(
font,
text,
paragraph_start,
text.len(),
max_width_px,
letter_spacing_px,
&mut lines,
);
let metrics = font.line_metrics();
let line_count = lines.len() as i32;
let used_height = if line_count == 0 {
0
} else {
line_count * metrics.line_height as i32 + (line_count - 1) * line_spacing_px as i32
};
WrappedText { lines, used_height }
}
fn wrap_span<F: FontMetrics + ?Sized>(
font: &F,
text: &str,
start: usize,
end: usize,
max_width_px: i32,
letter_spacing_px: i8,
out: &mut Vec<WrappedLine>,
) {
if start == end {
push_line(font, text, start, end, letter_spacing_px, out);
return;
}
let max_width_fp16 = max_width_px.max(0) * 16;
let mut line_start = skip_leading_spaces(text, start, end);
while line_start < end {
let mut line_end = line_start;
let mut last_break: Option<usize> = None;
let mut overflow_at: Option<usize> = None;
for (rel, ch) in text[line_start..end].char_indices() {
let abs = line_start + rel;
let candidate_end = abs + ch.len_utf8();
let width =
measure_text_fp16(font, &text[line_start..candidate_end], letter_spacing_px);
if width > max_width_fp16 {
overflow_at = Some(abs);
break;
}
line_end = candidate_end;
if is_soft_break(ch) {
last_break = Some(candidate_end);
}
}
match overflow_at {
None => {
push_line(
font,
text,
line_start,
trim_trailing_spaces(text, line_start, line_end),
letter_spacing_px,
out,
);
break;
}
Some(overflow) => {
if let Some(break_after) = last_break
&& break_after > line_start
{
let soft_end = trim_trailing_spaces(text, line_start, break_after);
push_line(font, text, line_start, soft_end, letter_spacing_px, out);
line_start = skip_leading_spaces(text, break_after, end);
} else {
let hard_end = if overflow == line_start {
next_char_end(text, line_start, end).unwrap_or(end)
} else {
overflow
};
push_line(font, text, line_start, hard_end, letter_spacing_px, out);
line_start = skip_leading_spaces(text, hard_end, end);
}
}
}
}
}
fn push_line<F: FontMetrics + ?Sized>(
font: &F,
text: &str,
start: usize,
end: usize,
letter_spacing_px: i8,
out: &mut Vec<WrappedLine>,
) {
let advance_fp16 = measure_text_fp16(font, &text[start..end], letter_spacing_px);
out.push(WrappedLine {
start,
end,
advance_fp16,
});
}
fn glyph_advance_fp16<F: FontMetrics + ?Sized>(font: &F, ch: char) -> i32 {
font.glyph_metrics(ch)
.map(|info| info.advance_fp16 as i32)
.unwrap_or_else(|| fallback_advance_fp16(font))
}
fn fallback_advance_fp16<F: FontMetrics + ?Sized>(font: &F) -> i32 {
((font.line_metrics().line_height as i32 + 1) / 2) * 16
}
fn is_soft_break(ch: char) -> bool {
ch == ' ' || ch == '-' || is_zero_width_break(ch)
}
fn is_zero_width_break(ch: char) -> bool {
ch == '\u{200B}'
}
fn skip_leading_spaces(text: &str, mut start: usize, end: usize) -> usize {
while start < end {
let Some(ch) = text[start..end].chars().next() else {
break;
};
if ch != ' ' {
break;
}
start += ch.len_utf8();
}
start
}
fn trim_trailing_spaces(text: &str, start: usize, mut end: usize) -> usize {
while start < end {
let Some((idx, ch)) = text[start..end].char_indices().next_back() else {
break;
};
if ch != ' ' {
break;
}
end = start + idx;
}
end
}
fn next_char_end(text: &str, start: usize, end: usize) -> Option<usize> {
text[start..end]
.chars()
.next()
.map(|ch| start + ch.len_utf8())
}
#[cfg(test)]
mod widget_font_tests {
use super::*;
#[test]
fn unset_resolves_to_default_font() {
let wf = WidgetFont::new();
assert!(!wf.is_set());
let lm = wf.resolve().line_metrics();
assert_eq!(
lm.line_height,
crate::bitmap_font::FONT_6X10.line_metrics().line_height
);
}
#[test]
fn set_then_resolve_returns_assigned_font() {
let mut wf = WidgetFont::with_font(&crate::bitmap_font::FONT_6X10);
assert!(wf.is_set());
assert!(wf.resolve().measure_fp16("A") > 0);
wf.clear();
assert!(!wf.is_set());
}
}