fix(qt): stabilize Japanese font fallback

cx
This commit is contained in:
vicrodh committed 2026-08-28 02:57:48 -06:00
1 parent 3da21c2752
commit 0f69895f15
8 files changed
+107 -45

No files matched your search

+2 -2
View File
@@ -317,8 +317,8 @@ fn build_rhi_items() {
let moc = qtbuild.moc(&header, qt_build_utils::MocArguments::default());
cc.file(source).file(&moc.cpp);
}
// Plain helper, no QObject/moc: registers one script-specific bundled
// font through QFontDatabase before Main.qml constructs any text.
// Plain helper, no QObject/moc: registers script-specific bundled font
// fallbacks through QFontDatabase before Main.qml constructs any text.
println!("cargo:rerun-if-changed=cxx/font_fallback.cpp");
cc.file("cxx/font_fallback.cpp");
// Plain helper, no QObject/moc: enumerates the exact QVulkanInstance that
+24
View File
@@ -73,3 +73,27 @@ extern "C" bool qbz_register_devanagari_fallback()
installFontMessageFilter();
return true;
}
extern "C" bool qbz_register_japanese_fallback()
{
// LINE Seed JP is already bundled as a selectable UI/lyrics face. Make
// it Qt's deterministic fallback for the three scripts used by Japanese
// text as well. Without this, a Latin-only choice such as Source Sans 3
// delegates to the host's first fontconfig match; a user-installed
// MS Gothic can therefore receive synthetic Medium/Bold and render much
// heavier than the surrounding UI.
const auto id = QFontDatabase::addApplicationFont(QStringLiteral(
":/qt/qml/com/blitzfc/qbz/qml/assets/fonts/"
"LINESeedJP-Regular.ttf"));
if (id < 0)
return false;
const auto families = QFontDatabase::applicationFontFamilies(id);
if (families.isEmpty())
return false;
const auto &family = families.constFirst();
QFontDatabase::addApplicationFallbackFontFamily(QChar::Script_Han, family);
QFontDatabase::addApplicationFallbackFontFamily(QChar::Script_Hiragana, family);
QFontDatabase::addApplicationFallbackFontFamily(QChar::Script_Katakana, family);
return true;
}
+8 -4
View File
@@ -20,8 +20,8 @@
// Hence this document: `QQmlApplicationEngine::load` returns with its
// FontLoader already in the font database (verified: the family is present
// the instant load() returns), so main.rs can set the application font in the
// gap between this document and Main.qml. "System" maps to an empty source:
// no UI family is loaded and Qt keeps the operating-system default.
// gap between this document and Main.qml. "System" leaves the Loader inactive:
// no empty URL is assigned and Qt keeps the operating-system default.
//
// It is deliberately NON-VISUAL — a QtObject, not a Window — so loading it
// adds a root object and nothing else.
@@ -46,7 +46,11 @@ QtObject {
return "assets/fonts/SourceSans3-VariableFont_wght.ttf"
return ""
}
readonly property FontLoader selected: FontLoader {
source: root.selectedSource
readonly property Component selectedComponent: Component {
FontLoader { source: root.selectedSource }
}
readonly property Loader selected: Loader {
active: root.selectedSource !== ""
sourceComponent: root.selectedComponent
}
}
@@ -58,17 +58,26 @@ Item {
Math.min(root.width * 0.026, 48)))
readonly property string fontDir: "qrc:/qt/qml/com/blitzfc/qbz/qml/assets/fonts/"
FontLoader { id: fLineSeed; source: QbzLyrics.fontIndex === 1 ? root.fontDir + "LINESeedJP-Regular.ttf" : "" }
FontLoader { id: fMontserrat; source: QbzLyrics.fontIndex === 2 ? root.fontDir + "Montserrat-VariableFont_wght.ttf" : "" }
FontLoader { id: fNoto; source: QbzLyrics.fontIndex === 3 ? root.fontDir + "NotoSans-VariableFont_wdth,wght.ttf" : "" }
FontLoader { id: fSource; source: QbzLyrics.fontIndex === 4 ? root.fontDir + "SourceSans3-VariableFont_wght.ttf" : "" }
readonly property string fontFamily: {
if (QbzLyrics.fontIndex === 1) return fLineSeed.status === FontLoader.Ready ? fLineSeed.name : ""
if (QbzLyrics.fontIndex === 2) return fMontserrat.status === FontLoader.Ready ? fMontserrat.name : ""
if (QbzLyrics.fontIndex === 3) return fNoto.status === FontLoader.Ready ? fNoto.name : ""
if (QbzLyrics.fontIndex === 4) return fSource.status === FontLoader.Ready ? fSource.name : ""
readonly property string fontSource: {
if (QbzLyrics.fontIndex === 1) return root.fontDir + "LINESeedJP-Regular.ttf"
if (QbzLyrics.fontIndex === 2) return root.fontDir + "Montserrat-VariableFont_wght.ttf"
if (QbzLyrics.fontIndex === 3) return root.fontDir + "NotoSans-VariableFont_wdth,wght.ttf"
if (QbzLyrics.fontIndex === 4) return root.fontDir + "SourceSans3-VariableFont_wght.ttf"
return ""
}
Component {
id: fontLoaderComponent
FontLoader { source: root.fontSource }
}
Loader {
id: selectedFontLoader
active: root.fontSource !== ""
sourceComponent: fontLoaderComponent
}
readonly property string fontFamily:
selectedFontLoader.item
&& selectedFontLoader.item.status === FontLoader.Ready
? selectedFontLoader.item.name : ""
// --- Entry-animation retrigger (:57-74) --------------------------------
readonly property int watchedIndex: root.activeIndex
@@ -86,17 +86,26 @@ Item {
1.0 - root.ambientPrimary.b, 1.0), 1.18)
readonly property string fontDir: "qrc:/qt/qml/com/blitzfc/qbz/qml/assets/fonts/"
FontLoader { id: fLineSeed; source: QbzLyrics.fontIndex === 1 ? root.fontDir + "LINESeedJP-Regular.ttf" : "" }
FontLoader { id: fMontserrat; source: QbzLyrics.fontIndex === 2 ? root.fontDir + "Montserrat-VariableFont_wght.ttf" : "" }
FontLoader { id: fNoto; source: QbzLyrics.fontIndex === 3 ? root.fontDir + "NotoSans-VariableFont_wdth,wght.ttf" : "" }
FontLoader { id: fSource; source: QbzLyrics.fontIndex === 4 ? root.fontDir + "SourceSans3-VariableFont_wght.ttf" : "" }
readonly property string lyricFontFamily: {
if (QbzLyrics.fontIndex === 1) return fLineSeed.status === FontLoader.Ready ? fLineSeed.name : ""
if (QbzLyrics.fontIndex === 2) return fMontserrat.status === FontLoader.Ready ? fMontserrat.name : ""
if (QbzLyrics.fontIndex === 3) return fNoto.status === FontLoader.Ready ? fNoto.name : ""
if (QbzLyrics.fontIndex === 4) return fSource.status === FontLoader.Ready ? fSource.name : ""
readonly property string fontSource: {
if (QbzLyrics.fontIndex === 1) return root.fontDir + "LINESeedJP-Regular.ttf"
if (QbzLyrics.fontIndex === 2) return root.fontDir + "Montserrat-VariableFont_wght.ttf"
if (QbzLyrics.fontIndex === 3) return root.fontDir + "NotoSans-VariableFont_wdth,wght.ttf"
if (QbzLyrics.fontIndex === 4) return root.fontDir + "SourceSans3-VariableFont_wght.ttf"
return ""
}
Component {
id: fontLoaderComponent
FontLoader { source: root.fontSource }
}
Loader {
id: selectedFontLoader
active: root.fontSource !== ""
sourceComponent: fontLoaderComponent
}
readonly property string lyricFontFamily:
selectedFontLoader.item
&& selectedFontLoader.item.status === FontLoader.Ready
? selectedFontLoader.item.name : ""
function fmt(sec) {
sec = Math.max(0, Math.floor(sec))
+23 -14
View File
@@ -16,8 +16,8 @@
// moves of <= 2 lines, INSTANT for bigger jumps and the initial sync,
// 1:1 with Slint. The animation drives `contentY` only on the auto-follow
// path, so user wheel input is never animated.
// - the four embedded lyric font families (lazy: a FontLoader only gets a
// source once its option is selected).
// - the embedded lyric font families (lazy: one FontLoader exists only while
// a non-System option is selected).
//
// Unsynced/plain documents render as a static top-aligned list: no spacers,
// no active line, no scroll-follow.
@@ -60,23 +60,32 @@ Item {
readonly property real contentWidth: Math.max(0, width - 2 * sidePadding)
// ---- embedded lyric fonts (lazy) -------------------------------------
// ---- embedded lyric font (lazy) --------------------------------------
readonly property string fontDir: "qrc:/qt/qml/com/blitzfc/qbz/qml/assets/fonts/"
FontLoader { id: fLineSeed; source: view.fontIndex === 1 ? view.fontDir + "LINESeedJP-Regular.ttf" : "" }
FontLoader { id: fMontserrat; source: view.fontIndex === 2 ? view.fontDir + "Montserrat-VariableFont_wght.ttf" : "" }
FontLoader { id: fNoto; source: view.fontIndex === 3 ? view.fontDir + "NotoSans-VariableFont_wdth,wght.ttf" : "" }
FontLoader { id: fSource; source: view.fontIndex === 4 ? view.fontDir + "SourceSans3-VariableFont_wght.ttf" : "" }
readonly property string fontSource: {
if (view.fontIndex === 1) return view.fontDir + "LINESeedJP-Regular.ttf"
if (view.fontIndex === 2) return view.fontDir + "Montserrat-VariableFont_wght.ttf"
if (view.fontIndex === 3) return view.fontDir + "NotoSans-VariableFont_wdth,wght.ttf"
if (view.fontIndex === 4) return view.fontDir + "SourceSans3-VariableFont_wght.ttf"
return ""
}
Component {
id: fontLoaderComponent
FontLoader { source: view.fontSource }
}
Loader {
id: selectedFontLoader
active: view.fontSource !== ""
sourceComponent: fontLoaderComponent
}
// "" = the real Qt/system default = Slint's "System" option. A family
// that has not finished loading falls back to the default rather than
// rendering in a wrong face.
readonly property string fontFamily: {
if (view.fontIndex === 1) return fLineSeed.status === FontLoader.Ready ? fLineSeed.name : ""
if (view.fontIndex === 2) return fMontserrat.status === FontLoader.Ready ? fMontserrat.name : ""
if (view.fontIndex === 3) return fNoto.status === FontLoader.Ready ? fNoto.name : ""
if (view.fontIndex === 4) return fSource.status === FontLoader.Ready ? fSource.name : ""
return ""
}
readonly property string fontFamily:
selectedFontLoader.item
&& selectedFontLoader.item.status === FontLoader.Ready
? selectedFontLoader.item.name : ""
// ---- auto-center scroll ----------------------------------------------
// Last index centered on; -1 = nothing yet -> the next center is INSTANT.
+13 -6
View File
@@ -1,17 +1,24 @@
//! Script-specific application font fallbacks.
//!
//! The UI's "System" choice remains Qt's real system font. This module only
//! gives Qt a deterministic face when a selected UI font lacks Devanagari,
//! avoiding its noisy scan through unrelated installed families.
//! The UI's "System" choice remains Qt's real system font for covered glyphs.
//! This module only gives Qt deterministic bundled faces when that font (or a
//! selected UI/lyrics font) lacks Japanese or Devanagari, avoiding arbitrary
//! and sometimes visually incompatible host fallbacks.
unsafe extern "C" {
fn qbz_register_devanagari_fallback() -> bool;
fn qbz_register_japanese_fallback() -> bool;
}
pub(crate) fn register_devanagari_fallback() {
// SAFETY: the C++ function has no arguments, is called once on the GUI
// thread after QGuiApplication exists, and only mutates QFontDatabase's
pub(crate) fn register_script_fallbacks() {
// SAFETY: these C++ functions have no arguments, are called once on the
// GUI thread after QGuiApplication exists, and only mutate QFontDatabase's
// documented process-global application-font registry.
if unsafe { qbz_register_japanese_fallback() } {
log::info!("[qbz-qt] font fallback -> LINE Seed JP (Han/Hiragana/Katakana)");
} else {
log::warn!("[qbz-qt] bundled LINE Seed JP fallback failed to load");
}
if unsafe { qbz_register_devanagari_fallback() } {
log::info!("[qbz-qt] font fallback -> Noto Sans Devanagari");
} else {
+1 -1
View File
@@ -3422,7 +3422,7 @@ fn main() {
"qrc:/qt/qml/com/blitzfc/qbz/qml/FontPreload.qml",
));
}
font_qt::register_devanagari_fallback();
font_qt::register_script_fallbacks();
let app_font_family = settings_qt::app_font_family();
if !app_font_family.is_empty() {
if let Some(app) = app.as_mut() {