use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::OnceLock;
use lieui_text::TextSpec;
pub const ICON_FONT_FAMILY: &str = "Material Icons";
pub const ICON_FALLBACK: char = '□';
static FONT_BYTES: &[u8] = include_bytes!("assets/MaterialIcons-Regular.ttf");
static CODEPOINT_SRC: &str = include_str!("assets/MaterialIcons-Regular.codepoints");
pub fn codepoints() -> &'static HashMap<&'static str, u32> {
static MAP: OnceLock<HashMap<&'static str, u32>> = OnceLock::new();
MAP.get_or_init(|| {
CODEPOINT_SRC
.lines()
.filter_map(|line| {
let (name, hex) = line.split_once(' ')?;
u32::from_str_radix(hex.trim(), 16)
.ok()
.map(|cp| (name, cp))
})
.collect()
})
}
pub fn icon_char(name: &str) -> char {
codepoints()
.get(name)
.and_then(|cp| char::from_u32(*cp))
.unwrap_or(ICON_FALLBACK)
}
thread_local! {
static ICON_FAMILY: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub fn ensure_icon_font() {
ICON_FAMILY.with(|slot| {
let mut slot = slot.borrow_mut();
if slot.is_some() {
return;
}
let names = lieui_text::register_font_bytes(FONT_BYTES.to_vec());
*slot = Some(
names
.into_iter()
.find(|n| n == ICON_FONT_FAMILY)
.unwrap_or_else(|| {
eprintln!(
"[lieui] 图标字体注册结果不含 {ICON_FONT_FAMILY:?},图标将显示为占位字形"
);
ICON_FONT_FAMILY.to_string()
}),
);
});
}
pub fn icon_font_family() -> String {
ensure_icon_font();
ICON_FAMILY.with(|f| {
f.borrow()
.clone()
.unwrap_or_else(|| ICON_FONT_FAMILY.to_string())
})
}
pub fn icon_spec(font_size: f64) -> TextSpec {
TextSpec::new(font_size)
.font_family(icon_font_family())
.wrap(false)
}
#[cfg(test)]
mod tests {
use super::*;
use lieui_text::TextEngine;
#[test]
fn known_names_resolve_to_their_codepoints() {
assert_eq!(icon_char("close"), '\u{e5cd}');
assert_eq!(icon_char("settings"), '\u{e8b8}');
assert_eq!(icon_char("add"), '\u{e145}');
}
#[test]
fn unknown_names_fall_back_to_a_placeholder() {
assert_eq!(icon_char("no-such-icon-xyz"), ICON_FALLBACK);
assert_eq!(icon_char(""), ICON_FALLBACK);
}
#[test]
fn codepoint_table_covers_a_useful_subset() {
for name in ["home", "search", "menu", "delete", "favorite", "star", "arrow_back"] {
assert!(codepoints().contains_key(name), "{name} 不在表里");
}
}
#[test]
fn icon_font_registers_and_measures() {
ensure_icon_font();
assert_eq!(icon_font_family(), ICON_FONT_FAMILY);
let spec = icon_spec(20.0);
let (w, h) = TextEngine::measure_text(&icon_char("close").to_string(), &spec);
assert!(w > 0.0 && h > 0.0, "close 测得 {w}×{h}");
assert!(!spec.wrap);
assert!((w - 20.0).abs() < 0.5 && (h - 20.0).abs() < 0.5, "行盒应≈字号见方:{w}×{h}");
}
}