Merge pull request #298 from vangel76/fix/symbol-glyph-fallback
UI fonts: symbol fallback faces so interface symbols aren't boxes (#197)
This commit is contained in:
commit
fa33d0b90f
|
|
@ -240,6 +240,11 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
|||
add(format!("{d}/dejavu/DejaVuSans.ttf"), 0);
|
||||
add(format!("{d}/liberation/LiberationSans-Regular.ttf"), 0);
|
||||
}
|
||||
// Fedora keeps each family in its own package folder (#54).
|
||||
add("/usr/share/fonts/abattis-cantarell-fonts/Cantarell-Regular.otf".into(), 0);
|
||||
add("/usr/share/fonts/google-noto/NotoSans-Regular.ttf".into(), 0);
|
||||
add("/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf".into(), 0);
|
||||
add("/usr/share/fonts/liberation-sans-fonts/LiberationSans-Regular.ttf".into(), 0);
|
||||
}
|
||||
"ui-bold" => {
|
||||
add(format!("{mac_sys}/HelveticaNeue.ttc"), 1);
|
||||
|
|
@ -249,6 +254,9 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
|||
add(format!("{d}/dejavu/DejaVuSans-Bold.ttf"), 0);
|
||||
add(format!("{d}/liberation/LiberationSans-Bold.ttf"), 0);
|
||||
}
|
||||
add("/usr/share/fonts/google-noto/NotoSans-Bold.ttf".into(), 0);
|
||||
add("/usr/share/fonts/dejavu-sans-fonts/DejaVuSans-Bold.ttf".into(), 0);
|
||||
add("/usr/share/fonts/liberation-sans-fonts/LiberationSans-Bold.ttf".into(), 0);
|
||||
}
|
||||
"cell" | "cell-bold" | "cell-italic" | "cell-bold-italic" => {
|
||||
let (carlito, calibri_win, calibri_mac, arial_mac, arial_win, lib, dejavu) = match role {
|
||||
|
|
@ -422,7 +430,15 @@ fn fallback_faces(han: HanOrder) -> Vec<(&'static str, Vec<(String, u32)>)> {
|
|||
HanOrder::Japanese => (ja, zh),
|
||||
HanOrder::Chinese => (zh, ja),
|
||||
};
|
||||
vec![first, second, ("sys-cjk-hangul", cjk_hangul()), ("sys-thai", thai()), ("sys-hebrew", hebrew())]
|
||||
vec![
|
||||
first,
|
||||
second,
|
||||
("sys-cjk-hangul", cjk_hangul()),
|
||||
("sys-thai", thai()),
|
||||
("sys-hebrew", hebrew()),
|
||||
("sys-symbols", symbols()),
|
||||
("sys-symbols-2", symbols_more()),
|
||||
]
|
||||
}
|
||||
|
||||
/// Linux directories that hold CJK fonts across distributions: Debian/Ubuntu (`opentype/noto`,
|
||||
|
|
@ -547,6 +563,59 @@ fn hebrew() -> Vec<(String, u32)> {
|
|||
v
|
||||
}
|
||||
|
||||
/// Symbol faces behind the UI font. Interface fonts such as Noto Sans and Cantarell (and egui's
|
||||
/// built-in Ubuntu) lack arrows, check marks, geometric shapes and keyboard symbols (▾ ✓ ✕ ▲ ↳ ⇧),
|
||||
/// which then render as empty boxes (#197, #54). DejaVu Sans covers most of them; the paths cover
|
||||
/// Debian/Ubuntu, Fedora and Arch layouts. [`load_fallback`] skips the file if Hebrew already
|
||||
/// loaded it.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn symbols() -> Vec<(String, u32)> {
|
||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
||||
let mut v: Vec<(String, u32)> = vec![(format!("{win}\\seguisym.ttf"), 0), ("/System/Library/Fonts/Apple Symbols.ttf".into(), 0)];
|
||||
for d in [
|
||||
"/usr/share/fonts/truetype/dejavu",
|
||||
"/usr/share/fonts/dejavu-sans-fonts",
|
||||
"/usr/share/fonts/TTF",
|
||||
"/usr/share/fonts/dejavu",
|
||||
"/usr/share/fonts/truetype/ttf-dejavu",
|
||||
"/usr/local/share/fonts/dejavu",
|
||||
"/usr/local/share/fonts",
|
||||
] {
|
||||
v.push((format!("{d}/DejaVuSans.ttf"), 0));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// A second symbol face for what the first lacks (⌘ and other technical symbols): Noto Sans
|
||||
/// Symbols 2 where it is installed, or Segoe UI Symbol behind Segoe UI.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn symbols_more() -> Vec<(String, u32)> {
|
||||
let mut v: Vec<(String, u32)> = Vec::new();
|
||||
for d in [
|
||||
"/usr/share/fonts/noto",
|
||||
"/usr/share/fonts/truetype/noto",
|
||||
"/usr/share/fonts/opentype/noto",
|
||||
"/usr/share/fonts/google-noto-sans-symbols-2-fonts",
|
||||
"/usr/share/fonts/google-noto",
|
||||
"/usr/local/share/fonts/noto",
|
||||
] {
|
||||
v.push((format!("{d}/NotoSansSymbols2-Regular.ttf"), 0));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Whether any symbol fallback face is installed (tests skip their glyph checks without one).
|
||||
pub fn symbol_face_installed() -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
symbols().iter().chain(symbols_more().iter()).any(|(p, _)| std::path::Path::new(p).exists())
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(ctx: &egui::Context, dark: bool) {
|
||||
// Install both palettes before System can switch between the style slots.
|
||||
for theme in [egui::Theme::Light, egui::Theme::Dark] {
|
||||
|
|
@ -618,8 +687,14 @@ mod tests {
|
|||
#[test]
|
||||
fn language_reorders_only_the_han_faces() {
|
||||
let keys = |han| fallback_faces(han).into_iter().map(|(k, _)| k).collect::<Vec<_>>();
|
||||
assert_eq!(keys(HanOrder::Japanese), ["sys-cjk-han-ja", "sys-cjk-han-zh", "sys-cjk-hangul", "sys-thai", "sys-hebrew"]);
|
||||
assert_eq!(keys(HanOrder::Chinese), ["sys-cjk-han-zh", "sys-cjk-han-ja", "sys-cjk-hangul", "sys-thai", "sys-hebrew"]);
|
||||
assert_eq!(
|
||||
keys(HanOrder::Japanese),
|
||||
["sys-cjk-han-ja", "sys-cjk-han-zh", "sys-cjk-hangul", "sys-thai", "sys-hebrew", "sys-symbols", "sys-symbols-2"]
|
||||
);
|
||||
assert_eq!(
|
||||
keys(HanOrder::Chinese),
|
||||
["sys-cjk-han-zh", "sys-cjk-han-ja", "sys-cjk-hangul", "sys-thai", "sys-hebrew", "sys-symbols", "sys-symbols-2"]
|
||||
);
|
||||
assert_eq!(HanOrder::of(Language::Ja), HanOrder::Japanese);
|
||||
for l in [Language::En, Language::Zh, Language::Ko, Language::Ru, Language::PtBr, Language::De, Language::Cs] {
|
||||
assert_eq!(HanOrder::of(l), HanOrder::Chinese, "{l:?} shares the default order (no font rebuild)");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
//! The interface's symbol characters must not render as empty boxes (#197, #54): the UI font often
|
||||
//! lacks arrows, check marks and geometric shapes, so a symbol face backs it up.
|
||||
|
||||
use egui::{Color32, FontFamily, FontId};
|
||||
use gridcraft_ui_egui::theme;
|
||||
|
||||
/// Symbols the interface draws as text (buttons, menus, dialogs, tooltips, Insert › Symbol).
|
||||
const UI_SYMBOLS: &str = "▲▼▸▾✓✕←↑→↓↳↘↗⇧⌃⌘★•›≠≤≥∞";
|
||||
|
||||
#[test]
|
||||
fn ui_symbols_have_glyphs_when_a_symbol_face_is_installed() {
|
||||
if !theme::symbol_face_installed() {
|
||||
return; // a minimal machine without any symbol face; nothing to assert
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_fonts(theme::font_definitions());
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |_ui| {});
|
||||
out.textures_delta.clear(); // nothing consumes the atlas in a headless test
|
||||
for family in [FontFamily::Proportional, FontFamily::Name(theme::CELL.into())] {
|
||||
let font = FontId::new(14.0, family);
|
||||
let glyphs = |text: &str| {
|
||||
let galley = ctx.fonts_mut(|f| f.layout_no_wrap(text.to_owned(), font.clone(), Color32::BLACK));
|
||||
galley.rows.iter().flat_map(|r| r.glyphs.iter().map(|g| (g.chr, g.uv_rect))).collect::<Vec<_>>()
|
||||
};
|
||||
// A private-use code point no font covers renders as the replacement box.
|
||||
let tofu: Vec<_> = glyphs("\u{10FFFD}").into_iter().map(|(_, uv)| uv).collect();
|
||||
let missing: String = glyphs(UI_SYMBOLS).into_iter().filter(|(_, uv)| tofu.contains(uv)).map(|(c, _)| c).collect();
|
||||
assert!(missing.is_empty(), "these interface symbols render as boxes in {:?}: {missing}", font.family);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue