mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-04-06 16:33:53 +08:00
better looking fonts
This commit is contained in:
BIN
selfdrive/assets/fonts/OpFont-Bold.otf
Normal file
BIN
selfdrive/assets/fonts/OpFont-Bold.otf
Normal file
Binary file not shown.
BIN
selfdrive/assets/fonts/OpFont-Medium.otf
Normal file
BIN
selfdrive/assets/fonts/OpFont-Medium.otf
Normal file
Binary file not shown.
BIN
selfdrive/assets/fonts/OpFont-Regular.otf
Normal file
BIN
selfdrive/assets/fonts/OpFont-Regular.otf
Normal file
Binary file not shown.
BIN
selfdrive/assets/fonts/OpFont-SemiBold.otf
Normal file
BIN
selfdrive/assets/fonts/OpFont-SemiBold.otf
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user