fix(qbz-qt): honor system font with script fallback

cx
This commit is contained in:
vicrodh committed 2026-08-25 20:15:38 -06:00
1 parent 10869f76a7
commit 82953c72da
21 files changed
+256 -67

No files matched your search

+4
View File
@@ -317,6 +317,10 @@ 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.
println!("cargo:rerun-if-changed=cxx/font_fallback.cpp");
cc.file("cxx/font_fallback.cpp");
for p in qtbuild.include_paths() {
cc.include(p);
}
+75
View File
@@ -0,0 +1,75 @@
#include <QtCore/QChar>
#include <QtCore/QByteArray>
#include <QtCore/QMessageLogContext>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/qlogging.h>
#include <QtGui/QFontDatabase>
#include <cstdio>
namespace {
QtMessageHandler previousMessageHandler = nullptr;
bool messageFilterInstalled = false;
bool isExpectedDevanagariCapabilityWarning(
QtMsgType type, const QMessageLogContext &context, const QString &message)
{
return type == QtWarningMsg
&& context.category != nullptr
&& qstrcmp(context.category, "qt.text.font.db") == 0
&& message.startsWith(QStringLiteral("OpenType support missing for \""))
&& message.endsWith(QStringLiteral(", script 11"));
}
void fontMessageHandler(
QtMsgType type, const QMessageLogContext &context, const QString &message)
{
// Qt probes the selected UI face before consulting the application
// fallback. A face without Devanagari tables therefore emits this benign
// diagnostic even though the registered Noto face renders the text. Keep
// every other Qt diagnostic intact.
if (isExpectedDevanagariCapabilityWarning(type, context, message))
return;
if (previousMessageHandler != nullptr) {
previousMessageHandler(type, context, message);
return;
}
const auto rendered = qFormatLogMessage(type, context, message).toLocal8Bit();
std::fwrite(rendered.constData(), 1, rendered.size(), stderr);
std::fputc('\n', stderr);
std::fflush(stderr);
}
void installFontMessageFilter()
{
if (messageFilterInstalled)
return;
previousMessageHandler = qInstallMessageHandler(fontMessageHandler);
messageFilterInstalled = true;
}
} // namespace
extern "C" bool qbz_register_devanagari_fallback()
{
// This is an application font, not the UI font. Register it only for
// Devanagari so Qt never has to walk (and warn about) unrelated system
// families when catalog metadata contains that script.
const auto id = QFontDatabase::addApplicationFont(QStringLiteral(
":/qt/qml/com/blitzfc/qbz/qml/assets/fonts/"
"NotoSansDevanagari-VariableFont_wght.ttf"));
if (id < 0)
return false;
const auto families = QFontDatabase::applicationFontFamilies(id);
if (families.isEmpty())
return false;
QFontDatabase::addApplicationFallbackFontFamily(
QChar::Script_Devanagari, families.constFirst());
installFontMessageFilter();
return true;
}
+23 -14
View File
@@ -1,4 +1,4 @@
// FontPreload — registers the bundled typefaces BEFORE any Text exists.
// FontPreload — registers the SELECTED bundled typeface before any Text exists.
//
// WHY THIS FILE IS A SEPARATE DOCUMENT, loaded before Main.qml.
//
@@ -18,26 +18,35 @@
// is only registered once something loads its file.
//
// Hence this document: `QQmlApplicationEngine::load` returns with its
// FontLoaders already in the font database (verified: the family is present
// 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.
// 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.
//
// It is deliberately NON-VISUAL — a QtObject, not a Window — so loading it
// adds a root object and nothing else.
//
// The five faces are settings_qt::APP_FONT_VALUES, in that order. Inter is
// here too: it is the app's own face, named by Main.qml for the Controls, and
// it needs registering for exactly the same reason.
// The four non-system faces are settings_qt::APP_FONT_VALUES[1..], in order.
// Noto Sans Devanagari is registered separately by font_qt as a script-only
// fallback; it is never an application-font selection.
import QtQuick
import com.blitzfc.qbz
QtObject {
readonly property FontLoader inter: FontLoader { source: "assets/fonts/Inter_18pt-Regular.ttf" }
readonly property FontLoader interMedium: FontLoader { source: "assets/fonts/Inter_18pt-Medium.ttf" }
readonly property FontLoader interSemiBold: FontLoader { source: "assets/fonts/Inter_18pt-SemiBold.ttf" }
readonly property FontLoader interBold: FontLoader { source: "assets/fonts/Inter_18pt-Bold.ttf" }
readonly property FontLoader lineSeed: FontLoader { source: "assets/fonts/LINESeedJP-Regular.ttf" }
readonly property FontLoader montserrat: FontLoader { source: "assets/fonts/Montserrat-VariableFont_wght.ttf" }
readonly property FontLoader notoSans: FontLoader { source: "assets/fonts/NotoSans-VariableFont_wdth,wght.ttf" }
readonly property FontLoader sourceSans3: FontLoader { source: "assets/fonts/SourceSans3-VariableFont_wght.ttf" }
id: root
readonly property string selectedSource: {
if (QbzShell.appFontFamily === "LINE Seed JP")
return "assets/fonts/LINESeedJP-Regular.ttf"
if (QbzShell.appFontFamily === "Montserrat")
return "assets/fonts/Montserrat-VariableFont_wght.ttf"
if (QbzShell.appFontFamily === "Noto Sans")
return "assets/fonts/NotoSans-VariableFont_wdth,wght.ttf"
if (QbzShell.appFontFamily === "Source Sans 3")
return "assets/fonts/SourceSans3-VariableFont_wght.ttf"
return ""
}
readonly property FontLoader selected: FontLoader {
source: root.selectedSource
}
}
+3 -26
View File
@@ -3,9 +3,9 @@
// sequence once the QML tree is complete (the bridge registers its Qt
// thread handle in that first invokable, so every async UI hop lands).
//
// Font: the Slint app bundles the Inter 18pt faces (app.slint); the same
// TTFs are embedded here via qrc and applied app-wide through the
// ApplicationWindow font (children inherit).
// Font: main.rs applies an explicitly selected bundled family before this
// document is built. With "System" it applies nothing, so both plain Text and
// Qt Quick Controls inherit the operating-system font chosen by Qt.
import QtQuick
import QtQuick.Controls
@@ -173,29 +173,6 @@ ApplicationWindow {
rightPadding: 0
color: "#1a1a1a"
FontLoader { id: interRegular; source: "assets/fonts/Inter_18pt-Regular.ttf" }
FontLoader { id: interMedium; source: "assets/fonts/Inter_18pt-Medium.ttf" }
FontLoader { id: interSemiBold; source: "assets/fonts/Inter_18pt-SemiBold.ttf" }
FontLoader { id: interBold; source: "assets/fonts/Inter_18pt-Bold.ttf" }
// The quality badge's face in the Tauri build — loading it here registers
// the family app-wide, so QualityBadge can name it without carrying its
// own loader into every row it renders in.
FontLoader { id: lineSeed; source: "assets/fonts/LINESeedJP-Regular.ttf" }
// Settings > Appearance > Typography & Language > Font — the CONTROLS half.
//
// The app's own text does not come from here: a plain `Text` takes the
// APPLICATION font at construction, which main() sets before this document
// is even loaded (see qml/FontPreload.qml for the measurements). What this
// line still owns is Qt Quick Controls, which follow ApplicationWindow.font
// and would otherwise sit on Inter while every label around them moved.
//
// "" = System, i.e. leave Qt's own choice alone, which is what this app
// rendered before the setting existed.
font.family: QbzShell.appFontFamily !== ""
? QbzShell.appFontFamily
: (interRegular.status === FontLoader.Ready ? interRegular.name : "Sans Serif")
// Phase 23: every domain singleton boots (registers its Qt-thread
// hop; QbzSession.boot additionally fires the app boot sequence).
// ── The renderer probe ────────────────────────────────────────────────
@@ -239,14 +239,12 @@ Column {
onSelected: function (i) { QbzBridge.settingsSelect("ui-scale", i) }
}
}
// The app-wide typeface. The families are the ones already BUNDLED —
// the same set the lyrics panel offers (shell/LyricsControlsFlyout.qml)
// and the same mechanism the Tauri build had — so picking one costs no
// download and cannot fail on a machine that lacks the face. "System"
// means the window default, which is Inter (Main.qml).
// The app-wide typeface. The four named families are bundled — the same
// set the lyrics panel offers (shell/LyricsControlsFlyout.qml) — while
// "System" deliberately leaves Qt's operating-system default untouched.
SettingRow {
label: QbzSession.tr("Font", QbzSession.trRev)
description: QbzSession.tr("The typeface used across the app. Only fonts shipped with QBZ are offered, so the choice looks the same on every machine (requires restart)", QbzSession.trRev)
description: QbzSession.tr("The typeface used across the app. System follows your operating system; the other choices are bundled with QBZ (requires restart)", QbzSession.trRev)
QbzSelect {
menuWidth: 200
options: root.doc.appFonts || []
+1 -1
View File
@@ -68,7 +68,7 @@ Column {
QbzTheme { id: theme }
// "" = the window default (Inter) — Qt resets an empty family to the
// "" = the Qt/system default — Qt resets an empty family to the
// application font, exactly Slint's empty font-family = "System" option.
readonly property string effFamily: row.fontFamily
+1 -1
View File
@@ -68,7 +68,7 @@ Item {
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" : "" }
// "" = the window default (Inter) = Slint's "System" option. A family
// "" = 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: {
+20
View File
@@ -0,0 +1,20 @@
//! 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.
unsafe extern "C" {
fn qbz_register_devanagari_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
// documented process-global application-font registry.
if unsafe { qbz_register_devanagari_fallback() } {
log::info!("[qbz-qt] font fallback -> Noto Sans Devanagari");
} else {
log::warn!("[qbz-qt] bundled Noto Sans Devanagari fallback failed to load");
}
}
+2
View File
@@ -71,6 +71,7 @@ mod playlist_picker_bridge;
// QbzFolderEdit and the shared playlist editor on QbzPlaylistEdit — so a
// folder save cannot perturb an open playlist editor.
mod folder_edit_bridge;
mod font_qt;
mod playlist_edit_bridge;
mod playlist_manager_bridge;
// Purchases (2026-08-15 purchases contract §G.1): the QbzPurchases singleton —
@@ -3249,6 +3250,7 @@ fn main() {
"qrc:/qt/qml/com/blitzfc/qbz/qml/FontPreload.qml",
));
}
font_qt::register_devanagari_fallback();
let app_font_family = settings_qt::app_font_family();
if !app_font_family.is_empty() {
if let Some(app) = app.as_mut() {
+3 -5
View File
@@ -1426,13 +1426,11 @@ const UI_SCALE_VALUES: &[&str] = &["xs", "small", "default", "large", "xl"];
/// The app-wide typeface (Settings > Appearance > Typography & Language).
///
/// The SAME five the lyrics panel offers (`lyrics_qt::FONTS`) and the same
/// slugs, because they are the same files: everything here is BUNDLED in
/// `qml/assets/fonts/`, so a choice cannot fail on a machine that does not
/// have the face installed, and it looks identical everywhere. That is the
/// Tauri build's mechanism, which is what the owner asked for.
/// slugs. The four named families are bundled in `qml/assets/fonts/`, so an
/// explicit choice cannot fail on a machine that lacks the face. "System" is
/// the exception: it deliberately leaves Qt's operating-system choice alone.
///
/// Only "System" is translated — a typeface name is a proper noun.
/// "System" resolves to the window default, which is Inter (Main.qml).
const APP_FONT_LABELS: &[&str] = &[
"System",
"LINE Seed JP",
@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/devanagari)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+10
View File
@@ -20,3 +20,13 @@ bundles them instead. The whole set is ~159 KB.
registering — dropping a file in is enough. That is also why this licence
lives HERE and not beside the SVGs: the sweep takes every file it finds, and
a markdown file has no business being a QML resource.
## Noto Sans Devanagari (SIL OFL 1.1)
`qml/assets/fonts/NotoSansDevanagari-VariableFont_wght.ttf` — the hinted,
weight-variable slim build from Noto Sans Devanagari v2.006. Source archive:
https://github.com/notofonts/devanagari.
QBZ registers this family only as Qt's application fallback for Devanagari;
it is not an interface-font option and never replaces the operating-system
font selected by “System”. Licence: `LICENSE-Noto-Sans-Devanagari.txt`.