better looking fonts

This commit is contained in:
Rick Lan
2026-02-11 20:14:06 +08:00
parent 0634122d32
commit bdf701af10
7 changed files with 116 additions and 36 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -11,7 +11,7 @@ LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
GLYPH_PADDING = 6
EXTRA_CHARS = "×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ×°§•€£¥"
UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
def _languages():
@@ -23,19 +23,25 @@ def _languages():
def _char_sets():
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
unifont = set(base)
labels = set(base)
per_lang: dict[str, tuple[int, ...]] = {}
for language, code in _languages().items():
unifont.update(language)
for prefix in ("app_", "dragonpilot_"):
po_path = TRANSLATIONS_DIR / f"{prefix}{code}.po"
try:
chars = set(po_path.read_text(encoding="utf-8"))
except FileNotFoundError:
continue
(unifont if code in UNIFONT_LANGUAGES else base).update(chars)
labels.update(language)
po_path = TRANSLATIONS_DIR / f"app_{code}.po"
try:
chars = set(po_path.read_text(encoding="utf-8"))
except FileNotFoundError:
continue
if code in UNIFONT_LANGUAGES:
lang_chars = set(base) | chars
per_lang[code] = tuple(sorted(ord(c) for c in lang_chars))
else:
base.update(chars)
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
base_cp = tuple(sorted(ord(c) for c in base))
labels_cp = tuple(sorted(ord(c) for c in labels))
return base_cp, labels_cp, per_lang
def _glyph_metrics(glyphs, rects, codepoints):
@@ -87,12 +93,10 @@ def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_h
path.write_text("\n".join(lines) + "\n")
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
print(f"Processing {font_path.name}...")
font_size = {
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
}.get(font_path.name, 200)
def _process_font(font_path: Path, codepoints: tuple[int, ...], output_name: str | None = None):
stem = output_name or font_path.stem
font_size = 48 if font_path.stem.lower().startswith("opfont") else 200
print(f"Processing {font_path.name} -> {stem} ({len(codepoints)} glyphs @ {font_size}px)...")
data = font_path.read_bytes()
file_buf = rl.ffi.new("unsigned char[]", data)
@@ -108,24 +112,42 @@ def _process_font(font_path: Path, codepoints: tuple[int, ...]):
raise RuntimeError("raylib returned an empty atlas")
rects = rects_ptr[0]
atlas_name = f"{font_path.stem}.png"
atlas_name = f"{stem}.png"
atlas_path = FONT_DIR / atlas_name
entries, line_height, base = _glyph_metrics(glyphs, rects, codepoints)
if not rl.export_image(image, atlas_path.as_posix()):
raise RuntimeError("Failed to export atlas image")
_write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries)
_write_bmfont(FONT_DIR / f"{stem}.fnt", font_size, stem, atlas_name, line_height, base, (image.width, image.height), entries)
def main():
base_cp, unifont_cp = _char_sets()
base_cp, labels_cp, per_lang = _char_sets()
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
opfonts: list[Path] = []
for font in fonts:
if "emoji" in font.name.lower():
if "emoji" in font.name.lower() or font.name == "unifont.otf":
continue
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
_process_font(font, glyphs)
if font.stem.lower().startswith("opfont"):
opfonts.append(font)
continue
_process_font(font, base_cp)
if not opfonts:
raise RuntimeError("OpFont not found (expected OpFont-*.otf in fonts dir)")
for opfont_path in opfonts:
weight = opfont_path.stem # e.g. "OpFont-Regular"
# Labels atlas: language display names + ASCII (for language selector)
_process_font(opfont_path, labels_cp, output_name=f"{weight}-Labels")
# Per-language atlases: ASCII + that language's .po chars
for lang_code, lang_cp in per_lang.items():
_process_font(opfont_path, lang_cp, output_name=f"{weight}-{lang_code}")
return 0

View File

@@ -97,6 +97,7 @@ class DeviceLayout(Widget):
if result == 1 and self._select_language_dialog:
selected_language = multilang.languages[self._select_language_dialog.selection]
multilang.change_language(selected_language)
gui_app.on_language_changed(selected_language)
self._update_calib_description()
self._select_language_dialog = None

View File

@@ -100,19 +100,45 @@ class FontWeight(StrEnum):
MEDIUM = "Inter-Medium.fnt"
BOLD = "Inter-Bold.fnt"
SEMI_BOLD = "Inter-SemiBold.fnt"
UNIFONT = "unifont.fnt"
UNIFONT = "OpFont-Regular-Labels.fnt"
# Small UI fonts
DISPLAY_REGULAR = "Inter-Regular.fnt"
ROMAN = "Inter-Regular.fnt"
DISPLAY = "Inter-Bold.fnt"
_OPFONT_WEIGHT = {
"Inter-Light.fnt": "Regular",
"Inter-Regular.fnt": "Regular",
"Inter-Medium.fnt": "Medium",
"Inter-SemiBold.fnt": "SemiBold",
"Inter-Bold.fnt": "Bold",
}
def _opfont_filename(inter_filename: str, lang_code: str) -> str:
"""Map an Inter font filename to the equivalent OpFont filename for a language."""
weight_name = _OPFONT_WEIGHT.get(inter_filename, "Regular")
return f"OpFont-{weight_name}-{lang_code}.fnt"
def font_fallback(font: rl.Font) -> rl.Font:
"""Fall back to unifont for languages that require it."""
if multilang.requires_unifont():
return gui_app.font(FontWeight.UNIFONT)
return font
"""Ensure the font is from the current language's font set.
Widgets may cache rl.Font references. After a language switch, those references
are stale (freed GPU texture). This catches them and returns the current equivalent.
"""
if not gui_app._font_remap:
return font # no language switch has occurred
# Check if this is a currently loaded font (handles texture ID reuse)
for f in gui_app._fonts.values():
if font.texture.id == f.texture.id:
return f
# Stale reference — look up the original weight and return current font for it
weight = gui_app._font_remap.get(font.texture.id)
if weight:
return gui_app.font(weight)
return gui_app.font(FontWeight.NORMAL)
@dataclass
@@ -204,6 +230,8 @@ class GuiApplication:
dp_ui_mici = False
self._width = width if width is not None else GuiApplication._default_width(dp_ui_mici)
self._height = height if height is not None else GuiApplication._default_height(dp_ui_mici)
self._active_lang_code: str = ""
self._font_remap: dict[int, FontWeight] = {} # old texture ID → weight (for stale references)
if PC and os.getenv("SCALE") is None:
self._scale = self._calculate_auto_scale()
@@ -439,6 +467,7 @@ class GuiApplication:
for font in self._fonts.values():
rl.unload_font(font)
self._fonts = {}
self._active_lang_code = ""
if self._render_texture is not None:
rl.unload_render_texture(self._render_texture)
@@ -548,6 +577,21 @@ class GuiApplication:
pass
def font(self, font_weight: FontWeight = FontWeight.NORMAL) -> rl.Font:
if font_weight not in self._fonts:
# For languages need unifont, load OpFont instead of Inter (except labels font)
if multilang.requires_unifont() and font_weight != FontWeight.UNIFONT:
filename = _opfont_filename(font_weight.value, self._active_lang_code)
else:
filename = font_weight.value
with as_file(FONT_DIR) as fspath:
fnt_path = fspath / filename
# Fall back to Regular weight if requested weight doesn't exist
if not fnt_path.exists() and multilang.requires_unifont():
filename = f"OpFont-Regular-{self._active_lang_code}.fnt"
fnt_path = fspath / filename
font = rl.load_font(fnt_path.as_posix())
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
self._fonts[font_weight] = font
return self._fonts[font_weight]
@property
@@ -586,14 +630,27 @@ class GuiApplication:
return False
def _load_fonts(self):
for font_weight_file in FontWeight:
with as_file(FONT_DIR) as fspath:
fnt_path = fspath / font_weight_file
font = rl.load_font(fnt_path.as_posix())
if font_weight_file != FontWeight.UNIFONT:
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
self._fonts[font_weight_file] = font
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
self._active_lang_code = multilang.language
rl.gui_set_font(self.font(FontWeight.NORMAL))
def on_language_changed(self, lang_code: str):
# Map old texture IDs → weights so we can remap stale references
old_weight_by_texture = {f.texture.id: w for w, f in self._fonts.items()}
old_fonts = list(self._fonts.values())
self._fonts = {}
self._active_lang_code = lang_code
# Load new fonts for all weights that were active
for weight in old_weight_by_texture.values():
self.font(weight)
# Carry forward existing remap + add new entries (weights are stable across switches)
self._font_remap = dict(self._font_remap) | {tid: w for tid, w in old_weight_by_texture.items()}
# Now safe to unload old fonts
for f in old_fonts:
rl.unload_font(f)
rl.gui_set_font(self.font(FontWeight.NORMAL))
from openpilot.system.ui.lib import text_measure, wrap_text
text_measure._cache.clear()
wrap_text._cache.clear()
def _set_styles(self):
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0)