fix(qbz-qt): honor system font with script fallback
cx
This commit is contained in:
21 files changed
+256
-67
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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 ────────────────────────────────────────────────
|
||||
|
||||
Binary file not shown.
@@ -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 || []
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
Vendored
+10
@@ -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`.
|
||||
Reference in new issue
Block a user