diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 642d998a0..d5e383d95 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -154,6 +154,7 @@ set(cockatrice_SOURCES src/interface/palette_editor/palette_generator.cpp src/interface/palette_editor/quick_setup_panel.cpp src/interface/palette_editor/palette_grid_widget.cpp + src/interface/palette_editor/font_grid_widget.cpp src/interface/palette_editor/palette_editor_dialog.cpp src/interface/widgets/cards/additional_info/color_identity_widget.cpp src/interface/widgets/cards/additional_info/deck_color_identity.cpp diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 0e8b6ced3..84e062cd7 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -3,6 +3,7 @@ #include "../../client/settings/cache_settings.h" #include "../../interface/card_localization.h" #include "../../interface/card_picture_loader/card_picture_loader.h" +#include "../../interface/theme_manager.h" #include "../card_dimensions.h" #include "../game_scene.h" #include "../z_values.h" @@ -154,7 +155,7 @@ void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &transla painter->rotate(angle); painter->translate(-translatedSize.width() / 2, -translatedSize.height() / 2); - QFont f; + QFont f = themeManager->appFont(AppFont::CardText); f.setPixelSize(fontSize * scale); painter->setFont(f); diff --git a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp index 50e80d359..3a740aa98 100644 --- a/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_graphics_item.cpp @@ -1,5 +1,7 @@ #include "abstract_graphics_item.h" +#include "../../interface/theme_manager.h" + #include #include #include @@ -20,7 +22,7 @@ void AbstractGraphicsItem::paintNumberEllipse(int number, painter->save(); QString numStr = QString::number(number); - QFont font("Serif"); + QFont font = themeManager->cardTextFont(); font.setPixelSize(fontSize); font.setWeight(QFont::Bold); diff --git a/cockatrice/src/game_graphics/board/counter_general.cpp b/cockatrice/src/game_graphics/board/counter_general.cpp index 4a8bbd6a4..23618e6bf 100644 --- a/cockatrice/src/game_graphics/board/counter_general.cpp +++ b/cockatrice/src/game_graphics/board/counter_general.cpp @@ -1,6 +1,7 @@ #include "counter_general.h" #include "../../interface/pixel_map_generator.h" +#include "../../interface/theme_manager.h" #include "abstract_graphics_item.h" #include @@ -41,7 +42,7 @@ void GeneralCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * / painter->drawPixmap(QPoint(0, 0), pixmap); if (value) { - QFont f("Serif"); + QFont f = themeManager->cardTextFont(); f.setPixelSize(qMax((int)(radius * scaleFactor), 10)); f.setWeight(QFont::Bold); painter->setPen(Qt::black); diff --git a/cockatrice/src/game_graphics/player/player_target.cpp b/cockatrice/src/game_graphics/player/player_target.cpp index 1cd97e7b5..0d10ba4ab 100644 --- a/cockatrice/src/game_graphics/player/player_target.cpp +++ b/cockatrice/src/game_graphics/player/player_target.cpp @@ -2,6 +2,7 @@ #include "../../client/settings/cache_settings.h" #include "../../interface/pixel_map_generator.h" +#include "../../interface/theme_manager.h" #include "../board/abstract_counter.h" #include "../board/abstract_graphics_item.h" #include "../board/arrow_target.h" @@ -76,7 +77,7 @@ void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /* QRectF translatedRect = path.controlPointRect(); QSize translatedSize = translatedRect.size().toSize(); - QFont font("Serif"); + QFont font = themeManager->cardTextFont(); font.setWeight(QFont::Bold); font.setPixelSize(qMax(qRound(translatedSize.height() / 1.3), 9)); painter->setFont(font); @@ -226,7 +227,7 @@ void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*o name = name.mid(0, 10) + "..."; } - QFont font; + QFont font = themeManager->appFont(AppFont::CardText); font.setPixelSize(qMax(qRound(translatedNameRect.height() / 1.5), 9)); painter->setFont(font); painter->setPen(Qt::white); diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.cpp b/cockatrice/src/interface/palette_editor/font_grid_widget.cpp new file mode 100644 index 000000000..0f01fbe09 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/font_grid_widget.cpp @@ -0,0 +1,127 @@ +#include "font_grid_widget.h" + +#include "../theme_manager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr int MIN_FONT_SIZE = 6; +static constexpr int MAX_FONT_SIZE = 72; +static constexpr int FALLBACK_FONT_SIZE = 10; + +static const QMap APP_FONT_DESCRIPTIONS = { + {AppFont::InterfaceDefault, QT_TR_NOOP("Default font for menus, dialogs and widgets")}, + {AppFont::ChatLog, QT_TR_NOOP("Font for chat messages and logs")}, + {AppFont::CardText, QT_TR_NOOP("Font for text rendered on cards")}, + {AppFont::DeckList, QT_TR_NOOP("Font for deck lists and the deck editor")}, +}; + +static QString roleDisplayName(AppFont::Role role) +{ + switch (role) { + case AppFont::InterfaceDefault: + return FontGridWidget::tr("Interface default"); + case AppFont::ChatLog: + return FontGridWidget::tr("Chat log"); + case AppFont::CardText: + return FontGridWidget::tr("Card text"); + case AppFont::DeckList: + return FontGridWidget::tr("Deck list"); + default: + return QString(); + } +} + +FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent) +{ + auto *grid = new QGridLayout(this); + grid->setSpacing(3); + grid->setContentsMargins(12, 8, 12, 8); + grid->setColumnStretch(0, 0); + grid->setColumnStretch(1, 1); + grid->setColumnStretch(2, 0); + + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(fontEnum.value(i)); + + auto *label = new QLabel(this); + grid->addWidget(label, i, 0); + + auto *familyCombo = new QFontComboBox(this); + grid->addWidget(familyCombo, i, 1); + connect(familyCombo, &QFontComboBox::currentFontChanged, this, [this] { emit fontsChanged(); }); + + auto *sizeSpin = new QSpinBox(this); + sizeSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE); + grid->addWidget(sizeSpin, i, 2); + connect(sizeSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); }); + + fontRows[role] = {label, familyCombo, sizeSpin}; + } + + retranslateUi(); +} + +void FontGridWidget::loadFonts(const FontConfig &cfg) +{ + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(fontEnum.value(i)); + const FontRow &row = fontRows[role]; + + // Roles absent from the file resolve the same way the runtime does: an unset + // role falls back to the config's InterfaceDefault, and only then to the + // startup font. appFont() would return the live preview instead, so Reset and + // a first load must not measure against it. + const QFont fallback = cfg.fonts.value(AppFont::InterfaceDefault, themeManager->defaultAppFont(role)); + const QFont font = cfg.fonts.value(role, fallback); + row.familyCombo->setCurrentFont(font); + + int size = font.pointSize(); + if (size <= 0) { + size = qApp->font().pointSize(); + } + if (size <= 0) { + size = FALLBACK_FONT_SIZE; + } + row.sizeSpin->setValue(qBound(MIN_FONT_SIZE, size, MAX_FONT_SIZE)); + } +} + +FontConfig FontGridWidget::currentFontConfig() const +{ + FontConfig cfg; + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(fontEnum.value(i)); + const FontRow &row = fontRows.value(role); + + // Canonical family + point size only: a fresh QFont carries default + // (non-bold, non-italic) weight/style, so nothing combo-side leaks in. + QFont font(row.familyCombo->currentFont().family()); + font.setPointSize(row.sizeSpin->value()); + cfg.fonts[role] = font; + } + return cfg; +} + +void FontGridWidget::retranslateUi() +{ + QMetaEnum fontEnum = QMetaEnum::fromType(); + for (int i = 0; i < fontEnum.keyCount(); ++i) { + auto role = static_cast(fontEnum.value(i)); + const FontRow &row = fontRows.value(role); + + row.label->setText(roleDisplayName(role)); + row.label->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, ""))); + row.familyCombo->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, ""))); + row.sizeSpin->setToolTip(tr("Point size")); + } +} diff --git a/cockatrice/src/interface/palette_editor/font_grid_widget.h b/cockatrice/src/interface/palette_editor/font_grid_widget.h new file mode 100644 index 000000000..8c80306d3 --- /dev/null +++ b/cockatrice/src/interface/palette_editor/font_grid_widget.h @@ -0,0 +1,43 @@ +#ifndef COCKATRICE_FONT_GRID_WIDGET_H +#define COCKATRICE_FONT_GRID_WIDGET_H + +#include "../theme_config.h" + +#include +#include +#include + +class QFontComboBox; +class QGridLayout; +class QLabel; +class QSpinBox; + +// Per-role font editors — one (family, point size) row per AppFont::Role. +// Owns the load/read round-trip for FontConfig but has no file I/O, mirroring +// PaletteGridWidget. Only family and point size are edited; weight and italic +// stay caller-side concerns and are stripped when reading back. +class FontGridWidget : public QWidget +{ + Q_OBJECT +public: + explicit FontGridWidget(QWidget *parent = nullptr); + + void loadFonts(const FontConfig &cfg); + FontConfig currentFontConfig() const; + void retranslateUi(); + +signals: + void fontsChanged(); + +private: + struct FontRow + { + QLabel *label = nullptr; + QFontComboBox *familyCombo = nullptr; + QSpinBox *sizeSpin = nullptr; + }; + + QMap fontRows; +}; + +#endif // COCKATRICE_FONT_GRID_WIDGET_H diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index b6271291d..09c81df5d 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -2,6 +2,7 @@ #include "../theme_config.h" #include "../theme_manager.h" +#include "font_grid_widget.h" #include "palette_generator.h" #include "palette_grid_widget.h" #include "quick_setup_panel.h" @@ -41,6 +42,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt // Load both scheme configs upfront so switching is instant loadSchemes(); + loadFonts(); loadedScheme = themeManager->isDarkMode(themeDirPath) ? "Dark" : "Light"; @@ -125,6 +127,32 @@ void PaletteEditorDialog::setupUi() paletteGrid->setVisible(false); root->addWidget(paletteGrid, 1); + // Font section — sibling to the palette grid, sharing its collapsible + // advanced area pattern. Fonts are shared per theme, not split by scheme. + fontToggleButton = new QPushButton(this); + fontToggleButton->setCheckable(true); + fontToggleButton->setChecked(false); + fontToggleButton->setFlat(true); + fontToggleButton->setStyleSheet("QPushButton { text-align: left; padding: 5px 12px; font-weight: bold; }" + "QPushButton:checked { }"); + root->addWidget(fontToggleButton); + + fontSeparator = makeSeparator(); + fontSeparator->setVisible(false); + root->addWidget(fontSeparator); + + fontSection = new QWidget; + auto *fontSectionLayout = new QVBoxLayout(fontSection); + fontSectionLayout->setContentsMargins(0, 0, 0, 0); + fontSectionLayout->setSpacing(0); + fontNoteLabel = new QLabel(fontSection); + fontNoteLabel->setContentsMargins(12, 4, 12, 0); + fontSectionLayout->addWidget(fontNoteLabel); + fontGrid = new FontGridWidget(fontSection); + fontSectionLayout->addWidget(fontGrid); + fontSection->setVisible(false); + root->addWidget(fontSection); + // Footer root->addWidget(makeSeparator()); footer = new QWidget; @@ -157,6 +185,7 @@ void PaletteEditorDialog::setupUi() connect(autoApplyTimer, &QTimer::timeout, this, &PaletteEditorDialog::onApply); connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &PaletteEditorDialog::onGenerateFromAccent); connect(paletteGrid, &PaletteGridWidget::paletteChanged, this, [this] { autoApplyTimer->start(); }); + connect(fontGrid, &FontGridWidget::fontsChanged, this, [this] { autoApplyTimer->start(); }); connect(revertButton, &QPushButton::clicked, this, &PaletteEditorDialog::onRevertToDefault); connect(resetBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onReset); connect(saveBtn, &QPushButton::clicked, this, &PaletteEditorDialog::onSave); @@ -166,15 +195,32 @@ void PaletteEditorDialog::setupUi() paletteGridToggleButton->setText(open ? tr("▼ Edit Palette") : tr("▶ Edit Palette")); paletteGridSeparator->setVisible(open); paletteGrid->setVisible(open); - - if (open) { - setMinimumHeight(680); - resize(width(), 680); - } else { - setMinimumHeight(220); - adjustSize(); // shrinks to fit just the visible content - } + updateAdvancedHeight(); }); + connect(fontToggleButton, &QPushButton::toggled, this, [this](bool open) { + fontToggleButton->setText(open ? tr("▼ Edit Fonts") : tr("▶ Edit Fonts")); + fontSeparator->setVisible(open); + fontSection->setVisible(open); + updateAdvancedHeight(); + }); +} + +void PaletteEditorDialog::updateAdvancedHeight() +{ + int height = 220; + if (paletteGridToggleButton->isChecked()) { + height += 460; + } + if (fontToggleButton->isChecked()) { + height += 200; + } + setMinimumHeight(height); + + if (paletteGrid->isVisible() || fontSection->isVisible()) { + resize(width(), height); + } else { + adjustSize(); // shrinks to fit just the visible content + } } void PaletteEditorDialog::retranslateUi() @@ -189,12 +235,16 @@ void PaletteEditorDialog::retranslateUi() editingLabel->setText(tr("Editing:")); paletteGridToggleButton->setText(tr("▶ Edit Palette")); paletteGridToggleButton->setToolTip(tr("Show or hide the per-role colour grid for manual tweaks")); + fontToggleButton->setText(fontSection->isVisible() ? tr("▼ Edit Fonts") : tr("▶ Edit Fonts")); + fontToggleButton->setToolTip(tr("Show or hide the per-purpose font settings for this theme")); + fontNoteLabel->setText(tr("Fonts belong to theme %1").arg(themeName)); + fontGrid->retranslateUi(); revertButton->setText(tr("↺ Revert to theme default")); resetBtn->setText(tr("Reset")); saveBtn->setText(tr("Save")); - resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette")); - saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower())); + resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette and fonts")); + saveBtn->setToolTip(tr("Write palette-%1.toml and fonts.toml, then reload the theme").arg(loadedScheme.toLower())); if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) { saveBtn->setEnabled(false); @@ -228,6 +278,19 @@ void PaletteEditorDialog::loadSchemes() } } +void PaletteEditorDialog::loadFonts() +{ + // Same user-dir > system-dir fallback ThemeManager uses: the shipped + // fonts.toml may live in the system theme directory rather than the + // resolved (user) theme directory. + FontConfig cfg = ThemeManager::loadFontConfig(themeDirPath); + if (!cfg.hasFonts()) { + cfg = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName); + } + savedFontConfig = cfg; + fontGrid->loadFonts(cfg); +} + void PaletteEditorDialog::seedAccentFromScheme(const QString &scheme) { QColor seed = workingConfig.value(scheme).colors.value(QPalette::Active).value(QPalette::Highlight); @@ -258,6 +321,7 @@ void PaletteEditorDialog::onGenerateFromAccent(const QColor &accent, int intensi void PaletteEditorDialog::onApply() { themeManager->previewPalette(paletteGrid->currentPaletteConfig(), loadedScheme); + themeManager->previewFonts(fontGrid->currentFontConfig()); } void PaletteEditorDialog::onSave() @@ -308,6 +372,17 @@ void PaletteEditorDialog::onSave() savedConfig[it.key()] = it.value(); } + // Persist the shared per-theme fonts when they changed. + const FontConfig fontCfg = fontGrid->currentFontConfig(); + if (fontCfg != savedFontConfig) { + if (!ThemeManager::saveFontConfig(saveDir, fontCfg)) { + QMessageBox::warning(this, tr("Save failed"), + tr("Could not write %1 to:\n%2").arg(FontConfig::fileName(), saveDir)); + return; + } + savedFontConfig = fontCfg; + } + themeManager->reloadCurrentTheme(); accept(); } @@ -316,6 +391,11 @@ void PaletteEditorDialog::onReset() { workingConfig[loadedScheme] = savedConfig[loadedScheme]; paletteGrid->loadPalette(savedConfig[loadedScheme]); + // Restore the live theme before re-reading the grid: loadFonts resolves + // unset roles, and the preview has currentFonts pointing at the edited + // config until the theme is reloaded. + themeManager->reloadCurrentTheme(); + fontGrid->loadFonts(savedFontConfig); seedAccentFromScheme(loadedScheme); } @@ -350,6 +430,18 @@ void PaletteEditorDialog::onRevertToDefault() workingConfig[loadedScheme] = def; paletteGrid->loadPalette(def); seedAccentFromScheme(loadedScheme); + + // Revert the shared per-theme fonts the same way: delete the user-written + // fonts.toml so the theme falls back to its shipped default, then reflect + // the resolved fonts in the editor. + QFile::remove(QDir(saveDir).absoluteFilePath(FontConfig::fileName())); + + // Reload the live theme so the font revert takes effect immediately. + themeManager->reloadCurrentTheme(); + + const FontConfig fontDef = ThemeManager::loadDefaultFontConfig(themeDirPath, themeName); + savedFontConfig = fontDef; + fontGrid->loadFonts(fontDef); } void PaletteEditorDialog::changeEvent(QEvent *e) diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h index e29a70cb0..0a9d47c6c 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.h +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.h @@ -15,6 +15,7 @@ class QComboBox; class QDialogButtonBox; class QPushButton; class PaletteGridWidget; +class FontGridWidget; class QuickSetupPanel; class QFrame; class QWidget; @@ -38,8 +39,10 @@ private: void setupUi(); void retranslateUi(); void refreshChromePalettes(); + void updateAdvancedHeight(); void loadScheme(const QString &scheme); // snapshot current, switch, load void seedAccentFromScheme(const QString &scheme); + void loadFonts(); // shared per theme, not split by light/dark // Sub-widgets QWidget *header; @@ -49,6 +52,11 @@ private: PaletteGridWidget *paletteGrid = nullptr; QPushButton *paletteGridToggleButton = nullptr; QFrame *paletteGridSeparator = nullptr; + FontGridWidget *fontGrid = nullptr; + QPushButton *fontToggleButton = nullptr; + QFrame *fontSeparator = nullptr; + QWidget *fontSection = nullptr; + QLabel *fontNoteLabel = nullptr; QWidget *footer; QComboBox *schemeComboBox = nullptr; QDialogButtonBox *buttonBox = nullptr; @@ -68,6 +76,10 @@ private: QMap workingConfig; QMap savedConfig; + // Fonts are shared per theme (fonts.toml, not split by light/dark), so a + // single saved config suffices; the live edits are read from fontGrid. + FontConfig savedFontConfig; + QTimer *autoApplyTimer = nullptr; protected: diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index 652e322c8..54143d7d3 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -304,4 +304,150 @@ QPalette PaletteConfig::apply(QPalette base) const } return base; -} \ No newline at end of file +} + +bool FontConfig::hasFonts() const +{ + return !fonts.isEmpty(); +} + +QString FontConfig::toToml() const +{ + QMetaEnum roleEnum = QMetaEnum::fromType(); + + QString entries; + + for (auto it = fonts.cbegin(); it != fonts.cend(); ++it) { + const char *roleName = roleEnum.valueToKey(it.key()); + + if (!roleName) { + continue; + } + + QString family = it.value().family().trimmed(); + int pointSize = it.value().pointSize(); + + if (family.isEmpty() || pointSize <= 0) { + continue; + } + + entries += QString("%1 = \"%2, %3\"\n").arg(QString(roleName), -20).arg(family).arg(pointSize); + } + + if (entries.isEmpty()) { + return {}; + } + + return "[Fonts]\n" + entries + "\n"; +} + +QString FontConfig::fileName() +{ + return "fonts.toml"; +} + +FontConfig FontConfig::fromFile(const QString &filePath) +{ + FontConfig cfg; + + QFile f(filePath); + + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return cfg; + } + + QMetaEnum roleEnum = QMetaEnum::fromType(); + + QString currentSection; + + QTextStream in(&f); + + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + + if (line.isEmpty() || line.startsWith('#') || line.startsWith(';')) { + continue; + } + + if (line.startsWith('[') && line.endsWith(']')) { + currentSection = line.mid(1, line.length() - 2).trimmed(); + continue; + } + + if (currentSection.compare("Fonts", Qt::CaseInsensitive) != 0) { + continue; + } + + int eq = line.indexOf('='); + + if (eq < 0) { + continue; + } + + QString key = line.left(eq).trimmed(); + QString value = line.mid(eq + 1).trimmed(); + + // Strip inline comments if preceded by whitespace + for (int i = 1; i < value.size(); ++i) { + if ((value[i] == '#' || value[i] == ';') && value[i - 1].isSpace()) { + value = value.left(i).trimmed(); + break; + } + } + + // Strip surrounding quotes, if any + if (value.size() >= 2 && + ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\'')))) { + value = value.mid(1, value.size() - 2).trimmed(); + } + + int comma = value.lastIndexOf(','); + + if (comma < 0) { + continue; + } + + QString family = value.left(comma).trimmed(); + + // Tolerate a partially-quoted family, e.g. "Inter", 10 + if (family.size() >= 2 && + ((family.startsWith('"') && family.endsWith('"')) || (family.startsWith('\'') && family.endsWith('\'')))) { + family = family.mid(1, family.size() - 2).trimmed(); + } + + bool ok = false; + int pointSize = value.mid(comma + 1).trimmed().toInt(&ok); + + if (!ok || family.isEmpty() || pointSize <= 0) { + continue; + } + + if (key.startsWith("AppFont::")) { + key = key.mid(QStringLiteral("AppFont::").size()); + } + + int roleInt = roleEnum.keyToValue(key.toUtf8().constData()); + + if (roleInt < 0) { + continue; + } + + // Canonical family + point size only: a fresh QFont carries default + // (non-bold, non-italic) weight/style, so nothing caller-side leaks in. + // No validation against installed fonts; Qt substitution applies. + QFont font(family); + font.setPointSize(pointSize); + cfg.fonts[static_cast(roleInt)] = font; + } + + return cfg; +} + +FontConfig FontConfig::fromThemeDir(const QString &themeDirPath) +{ + if (themeDirPath.isEmpty()) { + return {}; + } + + return fromFile(QDir(themeDirPath).absoluteFilePath(fileName())); +} diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 8b91613f5..84601a2bf 100644 --- a/cockatrice/src/interface/theme_config.h +++ b/cockatrice/src/interface/theme_config.h @@ -2,6 +2,7 @@ #define COCKATRICE_THEME_CONFIG_H #include +#include #include #include #include @@ -23,6 +24,23 @@ enum Role Q_ENUM_NS(Role) } // namespace AppColor +// Application-specific font roles, one configurable family + point size per +// purpose. Stored in a shared fonts.toml under a [Fonts] section (one file per +// theme, not split by light/dark). Only family and point size are stored; +// weight/italic are stripped on load and stay caller-side concerns. +namespace AppFont +{ +Q_NAMESPACE +enum Role +{ + InterfaceDefault, + ChatLog, + CardText, + DeckList, +}; +Q_ENUM_NS(Role) +} // namespace AppFont + struct ThemeConfig { QString colorScheme; @@ -60,4 +78,33 @@ struct PaletteConfig QPalette apply(QPalette base) const; }; -#endif // COCKATRICE_THEME_CONFIG_H \ No newline at end of file +/** @brief Per-purpose font configuration, stored as family + point size only. + * + * Serialized to a shared fonts.toml file with a [Fonts] section, e.g. + * @code + * [Fonts] + * InterfaceDefault = "Inter, 10" + * @endcode + */ +struct FontConfig +{ + QMap fonts; + + bool operator==(const FontConfig &rhs) const + { + return fonts == rhs.fonts; + } + bool operator!=(const FontConfig &rhs) const + { + return !(*this == rhs); + } + bool hasFonts() const; + QString toToml() const; + + static QString fileName(); + + static FontConfig fromFile(const QString &filePath); + static FontConfig fromThemeDir(const QString &themeDirPath); +}; + +#endif // COCKATRICE_THEME_CONFIG_H diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 893cdc795..fdd6bbc30 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -127,8 +128,10 @@ static QString usableDefaultStyle(const QString &style) ThemeManager::ThemeManager(QObject *parent) : QObject(parent) { defaultStyleName = usableDefaultStyle(qApp->style()->objectName()); - // Capture the untouched application palette before any theme is applied. + // Capture the untouched application palette and font before any theme is + // applied. defaultPalette = qApp->palette(); + defaultFont = qApp->font(); ensureThemeDirectoryExists(); #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this] { @@ -440,6 +443,46 @@ PaletteConfig ThemeManager::loadDefaultPaletteConfig(const QString &themeDirPath return cfg; } +FontConfig ThemeManager::loadFontConfig(const QString &themeDirPath) +{ + if (themeDirPath.isEmpty()) { + return {}; + } + return FontConfig::fromThemeDir(themeDirPath); +} + +FontConfig ThemeManager::loadDefaultFontConfig(const QString &themeDirPath, const QString &themeName) +{ + FontConfig cfg = FontConfig::fromThemeDir(themeDirPath); + if (!cfg.hasFonts()) { + // The shipped fonts.toml may live in the system theme directory rather + // than the resolved (user) theme directory, so built-in themes still + // get their curated fonts. + cfg = FontConfig::fromThemeDir(QDir(systemThemesBasePath()).absoluteFilePath(themeName)); + } + return cfg; +} + +bool ThemeManager::saveFontConfig(const QString &themeDirPath, const FontConfig &cfg) +{ + if (themeDirPath.isEmpty()) { + return false; + } + + QDir dir(themeDirPath); + if (!dir.exists()) { + dir.mkpath("."); + } + + QFile f(dir.absoluteFilePath(FontConfig::fileName())); + if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + return false; + } + + QTextStream(&f) << cfg.toToml(); + return true; +} + bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg) { if (!savePaletteConfig(themeDirPath, colorScheme, cfg)) { @@ -488,6 +531,24 @@ void ThemeManager::previewPalette(const PaletteConfig &cfg, const QString &schem applyStyleAndPalette(themeName, themeCfg, cfg, scheme); } +void ThemeManager::previewFonts(const FontConfig &cfg) +{ + currentFonts = cfg; + + // QApplication::setFont already sends a font change to every widget, so a + // repolish pass is not needed for fonts (unlike the palette path). + const auto it = cfg.fonts.constFind(AppFont::InterfaceDefault); + if (it != cfg.fonts.constEnd()) { + qApp->setFont(it.value()); + } else { + // A config without InterfaceDefault means "no override": restore the + // startup font instead of leaving the last preview applied. + qApp->setFont(defaultFont); + } + + emit fontsChanged(); +} + void ThemeManager::applyStyleAndPalette(const QString &themeName, const ThemeConfig &themeCfg, const PaletteConfig &palCfg, @@ -591,6 +652,44 @@ QColor ThemeManager::appColor(AppColor::Role role) const return accent; } +QFont ThemeManager::appFont(AppFont::Role role) const +{ + const auto it = currentFonts.fonts.constFind(role); + if (it != currentFonts.fonts.constEnd()) { + return it.value(); + } + + // Role fallbacks preserve the pre-theme defaults when no fonts.toml is + // present: an unset role resolves to the application font, so the result is + // identical to before. The report dialogs that used to hardcode monospace + // ask for it explicitly (report_utils::reportLogFont), and the counter + // painters that used to hardcode Serif ask for it through cardTextFont(). + return qApp->font(); +} + +bool ThemeManager::hasAppFont(AppFont::Role role) const +{ + return currentFonts.fonts.contains(role); +} + +QFont ThemeManager::cardTextFont() const +{ + if (hasAppFont(AppFont::CardText)) { + return appFont(AppFont::CardText); + } + QFont serifFont(QStringLiteral("Serif")); + serifFont.setStyleHint(QFont::Serif); + return serifFont; +} + +QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const +{ + // No per-role scaling and no preview: an unset role resolves to the font + // captured at startup, which is what the theme falls back to once the + // editor closes. + return defaultFont; +} + void ThemeManager::themeChangedSlot() { QString themeName = SettingsCache::instance().getThemeName(); @@ -642,6 +741,28 @@ void ThemeManager::themeChangedSlot() applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); + // ── Load fonts: shared per theme, no light/dark split ─────────────────── + FontConfig fonts = loadFontConfig(dirPath); + if (!fonts.hasFonts()) { + // The shipped fonts.toml may live in the system theme directory rather + // than the resolved (user) theme directory, so built-in themes still + // get their curated fonts. + const QString sysFontPath = QDir(systemThemesBasePath()).absoluteFilePath(themeName); + if (sysFontPath != dirPath) { + fonts = loadFontConfig(sysFontPath); + } + } + currentFonts = fonts; + const auto fontIt = currentFonts.fonts.constFind(AppFont::InterfaceDefault); + if (fontIt != currentFonts.fonts.constEnd()) { + qApp->setFont(fontIt.value()); + } else { + // No theme font: restore the pristine application font captured at + // startup, so a reverted preview cannot leak into the live theme. + qApp->setFont(defaultFont); + } + emit fontsChanged(); + QStringList resources; if (!dirPath.isEmpty()) { resources << dir.absolutePath(); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index c1ab65685..35cc36d04 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -47,10 +48,15 @@ private: // palette is applied. Used as the base when a theme supplies no palette, so // switching away from a custom palette restores the original colours. QPalette defaultPalette; + // Pristine application font captured at startup, before any theme font is + // applied. Restored when a theme (or a preview) supplies no InterfaceDefault + // font, so switching away from a themed font cannot leave it behind. + QFont defaultFont; QString currentThemePath; std::array brushes; QStringMap availableThemes; QMap currentAppColors; + FontConfig currentFonts; /* Internal cache for multiple backgrounds */ @@ -105,6 +111,9 @@ public: // Load/save per-scheme palette colors static PaletteConfig loadPaletteConfig(const QString &themeDirPath, const QString &colorScheme); static bool savePaletteConfig(const QString &themeDirPath, const QString &colorScheme, const PaletteConfig &cfg); + // Load/save shared per-theme fonts (fonts.toml, not split by scheme) + static FontConfig loadFontConfig(const QString &themeDirPath); + static bool saveFontConfig(const QString &themeDirPath, const FontConfig &cfg); // Resolve prefix to a scheme-qualified "theme:" path. Existence is probed // internally across the formats themes may ship (.png/.jpg/.svg), so // callers load the returned path directly. Prefers "-" @@ -123,6 +132,9 @@ public: // theme directory when it is absent from the resolved (user) directory. static PaletteConfig loadDefaultPaletteConfig(const QString &themeDirPath, const QString &themeName, const QString &colorScheme); + // Load the theme's shipped default fonts, falling back to the system theme + // directory when fonts.toml is absent from the resolved (user) directory. + static FontConfig loadDefaultFontConfig(const QString &themeDirPath, const QString &themeName); /** @brief Writes cfg to disk as the theme's palette-.toml and updates the * theme's stored colour scheme to match. Shared by PaletteEditorDialog::onSave * and FirstRunWizard's theme step so the two "generate + keep" paths can't drift. */ @@ -132,10 +144,25 @@ public: void reloadCurrentTheme(); void previewPalette(const PaletteConfig &cfg, const QString &scheme); + void previewFonts(const FontConfig &cfg); // Resolves an application color role: the theme's stored [AppColors] value // when present, otherwise a palette-accent-derived fallback. QColor appColor(AppColor::Role role) const; + /** @brief Resolves an application font role: the theme's stored [Fonts] + * value when present, otherwise a per-role fallback (serif basis + * for CardText, the application font for the remaining roles). */ + QFont appFont(AppFont::Role role) const; + /** @brief Whether the active theme defines this role in fonts.toml. */ + bool hasAppFont(AppFont::Role role) const; + /** @brief The CardText role with its historical Serif fallback. Used by the + * counter painters, which were Serif before the role existed. */ + QFont cardTextFont() const; + /** @brief Resolves an application font role against the startup font only, + * ignoring any previewed config in currentFonts. Editors that show + * "what this role resolves to" must use this, because appFont() + * reflects a live preview while the editor is open. */ + QFont defaultAppFont(AppFont::Role role) const; QBrush &getBgBrush(Role zone); QBrush getExtraBgBrush(Role zone, int zoneId = 0); @@ -144,6 +171,7 @@ protected slots: signals: void themeChanged(); void paletteChanged(); + void fontsChanged(); }; extern ThemeManager *themeManager; diff --git a/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp b/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp index c4b347047..8475a0ed9 100644 --- a/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp +++ b/cockatrice/src/interface/widgets/cards/art_crop_attribution.cpp @@ -1,5 +1,6 @@ #include "art_crop_attribution.h" +#include "../../theme_manager.h" #include "libcockatrice/card/printing/printing_info.h" #include @@ -35,7 +36,10 @@ QRectF paintArtAttribution(QPainter &painter, painter.save(); + // Family comes from the CardText theme role; the incoming painter size + // stays the basis for the scale factor exactly as before. QFont font = painter.font(); + font.setFamily(themeManager->appFont(AppFont::CardText).family()); font.setPointSizeF(qMax(6.0, font.pointSizeF() * scale)); painter.setFont(font); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 04fe9ab69..56fe0bda4 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -1,5 +1,7 @@ #include "card_info_picture_with_text_overlay_widget.h" +#include "../../theme_manager.h" + #include #include #include @@ -43,6 +45,7 @@ CardInfoPictureWithTextOverlayWidget::CardInfoPictureWithTextOverlayWidget(QWidg fontSize(fontSize), textAlignment(alignment), highlighted(false) { this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + connect(themeManager, &ThemeManager::fontsChanged, this, &CardInfoPictureWithTextOverlayWidget::applyCardTextFont); } /** @@ -97,6 +100,13 @@ void CardInfoPictureWithTextOverlayWidget::setTextAlignment(const Qt::Alignment update(); } +void CardInfoPictureWithTextOverlayWidget::applyCardTextFont() +{ + // The overlay paint reads the CardText role font at paint time, so a + // theme font change only needs a repaint here. + update(); +} + void CardInfoPictureWithTextOverlayWidget::setHighlighted(bool _highlighted) { if (highlighted == _highlighted) { @@ -168,8 +178,10 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event) return; } - // Calculate the optimal font size - QFont font = painter.font(); + // Calculate the optimal font size. The family comes from the CardText + // theme role; the fontSize member stays the starting size and the + // shrink-to-fit logic below is unchanged. + QFont font = themeManager->appFont(AppFont::CardText); int optimalFontSize = fontSize; // Start with the user-defined font size const QFontMetrics baseMetrics(font); int textWidth = pixmapRect.width(); @@ -284,7 +296,7 @@ QSize CardInfoPictureWithTextOverlayWidget::minimumSizeHint() const const QSize pixmapSize = pixmap.isNull() ? QSize(0, 0) : pixmap.size(); // Get the font metrics for the overlay text - QFont font; + QFont font = themeManager->appFont(AppFont::CardText); font.setPointSize(fontSize); const QFontMetrics fontMetrics(font); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h index 6c547dc13..41019f543 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.h @@ -47,6 +47,9 @@ protected: void mousePressEvent(QMouseEvent *event) override; [[nodiscard]] QSize minimumSizeHint() const override; +private slots: + void applyCardTextFont(); + private: void drawOutlinedText(QPainter &painter, const QRect &textRect, diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp index 2a2758ba5..a7387a068 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp @@ -1,11 +1,13 @@ #include "dlg_my_reports.h" +#include "../interface/theme_manager.h" #include "../utility/report_utils.h" #include "abstract_client.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/serverinfo_report.pb.h" #include +#include #include #include #include @@ -24,7 +26,6 @@ #include #include #include -#include #include #include @@ -69,7 +70,8 @@ DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent) auto *chatGroup = new QGroupBox(tr("Chat Log Context")); chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - chatLogEdit->setFont(QtUtils::monospaceFont()); + applyChatLogFont(); + connect(themeManager, &ThemeManager::fontsChanged, this, &DlgMyReports::applyChatLogFont); auto *chatLayout = new QVBoxLayout(chatGroup); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); @@ -122,6 +124,11 @@ DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent) refreshList(); } +void DlgMyReports::applyChatLogFont() +{ + chatLogEdit->setFont(report_utils::reportLogFont()); +} + void DlgMyReports::refreshList() { selectedReportIdBeforeRefresh = selectedReportId; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h index dc9f90d2d..90772f051 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h @@ -29,6 +29,7 @@ private slots: void reportDetailsResponse(const Response &response); void addComment(); void addCommentResponse(const Response &response); + void applyChatLogFont(); private: void loadReportDetails(const ServerInfo_Report &report); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp index 3ef45020b..c7c32469b 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp @@ -1,10 +1,13 @@ #include "dlg_report_user.h" +#include "../interface/theme_manager.h" +#include "../utility/report_utils.h" #include "abstract_client.h" #include "libcockatrice/protocol/pb/response.pb.h" #include #include +#include #include #include #include @@ -91,7 +94,8 @@ DlgReportUser::DlgReportUser(AbstractClient *_client, auto *chatLayout = new QVBoxLayout(chatGroup); chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - chatLogEdit->setFont(QtUtils::monospaceFont()); + applyChatLogFont(); + connect(themeManager, &ThemeManager::fontsChanged, this, &DlgReportUser::applyChatLogFont); if (!_autoChatLog.isEmpty()) { chatLogEdit->setPlainText(_autoChatLog); } else { @@ -120,6 +124,11 @@ DlgReportUser::DlgReportUser(AbstractClient *_client, layout->addWidget(buttonBox); } +void DlgReportUser::applyChatLogFont() +{ + chatLogEdit->setFont(report_utils::reportLogFont()); +} + void DlgReportUser::actSubmit() { const QString description = descriptionEdit->toPlainText().trimmed(); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h index c7fce055d..0f2fa5080 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_report_user.h @@ -27,6 +27,7 @@ public: private slots: void actSubmit(); void reportResponse(const Response &response); + void applyChatLogFont(); private: static QString categoryLabel(const QString &key); diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp index bad88ddbb..13a43bd47 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp @@ -1,5 +1,6 @@ #include "all_zones_card_amount_widget.h" +#include "../../theme_manager.h" #include "../general/display/shadow_background_label.h" #include "card_amount_widget.h" #include "libcockatrice/card/card_info.h" @@ -67,6 +68,7 @@ AllZonesCardAmountWidget::AllZonesCardAmountWidget(QWidget *parent, buttonBoxSideboard->setVisible(!isToken); connect(cardSizeSlider, &QSlider::valueChanged, this, &AllZonesCardAmountWidget::adjustFontSize); + connect(themeManager, &ThemeManager::fontsChanged, this, &AllZonesCardAmountWidget::applyCardTextFont); QTimer::singleShot(10, this, [this]() { adjustFontSize(this->cardSizeSlider->value()); }); @@ -90,8 +92,9 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) int newFontSize = minFontSize + (scalePercentage - basePercentage) * (maxFontSize - minFontSize) / 225; newFontSize = std::clamp(newFontSize, minFontSize, maxFontSize); - // Update the font labels - QFont zoneLabelFont = zoneLabelMainboard->font(); + // Update the font labels. The family comes from the CardText theme + // role; the slider-driven point size stays as before. + QFont zoneLabelFont = themeManager->appFont(AppFont::CardText); zoneLabelFont.setPointSize(newFontSize); zoneLabelMainboard->setFont(zoneLabelFont); zoneLabelSideboard->setFont(zoneLabelFont); @@ -101,6 +104,11 @@ void AllZonesCardAmountWidget::adjustFontSize(int scalePercentage) repaint(); } +void AllZonesCardAmountWidget::applyCardTextFont() +{ + adjustFontSize(cardSizeSlider->value()); +} + void AllZonesCardAmountWidget::setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount) { buttonBoxMainboard->setAmount(mainboardAmount); diff --git a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h index cefabe293..3213cf16b 100644 --- a/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/all_zones_card_amount_widget.h @@ -36,6 +36,9 @@ public slots: void adjustFontSize(int scalePercentage); void setAmounts(int mainboardAmount, int sideboardAmount, int tokensboardAmount); +private slots: + void applyCardTextFont(); + private: QVBoxLayout *layout; QSlider *cardSizeSlider; diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp index eae49d7ef..7f25ea1f4 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.cpp @@ -1,5 +1,6 @@ #include "card_amount_widget.h" +#include "../../theme_manager.h" #include "../deck_editor/deck_state_manager.h" #include "../general/display/dynamic_font_size_push_button.h" #include "deck_list_model.h" @@ -77,6 +78,7 @@ CardAmountWidget::CardAmountWidget(QWidget *parent, // Connect slider for dynamic font size adjustment connect(cardSizeSlider, &QSlider::valueChanged, this, &CardAmountWidget::adjustFontSize); + connect(themeManager, &ThemeManager::fontsChanged, this, &CardAmountWidget::applyCardTextFont); } int CardAmountWidget::getAmount() @@ -138,8 +140,9 @@ void CardAmountWidget::adjustFontSize(int scalePercentage) int newFontSize = minFontSize + (scalePercentage - basePercentage) * (maxFontSize - minFontSize) / 225; newFontSize = std::clamp(newFontSize, minFontSize, maxFontSize); - // Update the font for card count label - QFont cardCountFont = cardCountInZone->font(); + // Update the font for card count label. The family comes from the + // CardText theme role; the slider-driven point size stays as before. + QFont cardCountFont = themeManager->appFont(AppFont::CardText); cardCountFont.setPointSize(newFontSize); cardCountInZone->setFont(cardCountFont); @@ -150,6 +153,11 @@ void CardAmountWidget::adjustFontSize(int scalePercentage) repaint(); } +void CardAmountWidget::applyCardTextFont() +{ + adjustFontSize(cardSizeSlider->value()); +} + /** * @brief Updates the card count display in the widget. */ diff --git a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h index 6f5889fc7..cef872157 100644 --- a/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/card_amount_widget.h @@ -67,6 +67,7 @@ private slots: void removePrintingSideboard(); void removePrintingTokensboard(); void adjustFontSize(int scalePercentage); + void applyCardTextFont(); }; #endif // CARD_AMOUNT_WIDGET_H diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp index 037a4a1e5..c57f65352 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.cpp @@ -4,6 +4,7 @@ #include "../../client/sound_engine.h" #include "../../interface/card_localization.h" #include "../../interface/pixel_map_generator.h" +#include "../../interface/theme_manager.h" #include "../../tabs/tab_supervisor.h" #include "../user/user_context_menu.h" #include "../user/user_list_manager.h" @@ -63,6 +64,9 @@ ChatView::ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _sho connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &ChatView::adjustColorsToPalette); + applyChatFont(); + connect(themeManager, &ThemeManager::fontsChanged, this, &ChatView::applyChatFont); + userContextMenu = new UserContextMenu(tabSupervisor, this, game); connect(userContextMenu, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool))); @@ -108,6 +112,16 @@ void ChatView::adjustColorsToPalette() QTimer::singleShot(0, this, &ChatView::refreshBlockColors); } +void ChatView::applyChatFont() +{ + // Family comes from the ChatLog theme role; the message char formats carry + // no family of their own, so they (and new content) follow the widget and + // document default fonts. Weights/italics stay caller-side as before. + const QFont chatFont = themeManager->appFont(AppFont::ChatLog); + setFont(chatFont); + document()->setDefaultFont(chatFont); +} + void ChatView::refreshBlockColors() { QTextDocument *doc = document(); diff --git a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h index 3fa7224e5..a2e2f33dd 100644 --- a/cockatrice/src/interface/widgets/server/chat_view/chat_view.h +++ b/cockatrice/src/interface/widgets/server/chat_view/chat_view.h @@ -111,6 +111,7 @@ private slots: void openLink(const QUrl &link); void actMessageClicked(); void adjustColorsToPalette(); + void applyChatFont(); void refreshBlockColors(); void onScrollBarRangeChanged(); void onScrollBarValueChanged(int value); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp index 97383534f..3720aeb55 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp @@ -1,5 +1,6 @@ #include "archidekt_deck_preview_image_display_widget.h" +#include "../../../../../theme_manager.h" #include "../../../../general/display/shadow_background_label.h" #include @@ -17,13 +18,9 @@ ArchidektDeckPreviewImageDisplayWidget::ArchidektDeckPreviewImageDisplayWidget(Q bottomLeftLabel = new ShadowBackgroundLabel(this, ""); bottomRightLabel = new ShadowBackgroundLabel(this, ""); - QFont f; - f.setBold(true); - f.setPointSize(16); - topLeftLabel->setFont(f); - topRightLabel->setFont(f); - bottomLeftLabel->setFont(f); - bottomRightLabel->setFont(f); + applyCardTextFont(); + connect(themeManager, &ThemeManager::fontsChanged, this, + &ArchidektDeckPreviewImageDisplayWidget::applyCardTextFont); // Raise so labels appear above image topLeftLabel->raise(); @@ -32,6 +29,18 @@ ArchidektDeckPreviewImageDisplayWidget::ArchidektDeckPreviewImageDisplayWidget(Q bottomRightLabel->raise(); } +void ArchidektDeckPreviewImageDisplayWidget::applyCardTextFont() +{ + // Family comes from the CardText theme role; size and weight stay caller-side as before. + QFont f = themeManager->appFont(AppFont::CardText); + f.setBold(true); + f.setPointSize(16); + topLeftLabel->setFont(f); + topRightLabel->setFont(f); + bottomLeftLabel->setFont(f); + bottomRightLabel->setFont(f); +} + void ArchidektDeckPreviewImageDisplayWidget::setAspectRatio(float ratio) { aspectRatio = ratio; diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h index 991f27b6a..41585e851 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.h @@ -60,6 +60,9 @@ protected: */ void resizeEvent(QResizeEvent *event) override; +private slots: + void applyCardTextFont(); + private: float aspectRatio = 1.0f; ///< Aspect ratio to maintain for the preview image }; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp index b2ddace01..2b76de973 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.cpp @@ -1,5 +1,6 @@ #include "edhrec_api_response_card_details_display_widget.h" +#include "../../../../../../theme_manager.h" #include "../../../../../cards/card_info_picture_widget.h" #include "../../../../../cards/card_size_widget.h" #include "../../../../../general/display/background_plate_widget.h" @@ -9,6 +10,7 @@ #include "edhrec_api_response_card_synergy_display_widget.h" #include "libcockatrice/card/database/card_database_querier.h" +#include #include #include #include @@ -32,7 +34,9 @@ EdhrecApiResponseCardDetailsDisplayWidget::EdhrecApiResponseCardDetailsDisplayWi nameLabel = new QLabel(this); nameLabel->setText(toDisplay.name); nameLabel->setAlignment(Qt::AlignHCenter); - nameLabel->setStyleSheet("font-size: 20px; font-weight: bold"); + applyCardTextFont(); + connect(themeManager, &ThemeManager::fontsChanged, this, + &EdhrecApiResponseCardDetailsDisplayWidget::applyCardTextFont); inclusionDisplayWidget = new EdhrecApiResponseCardInclusionDisplayWidget(this, toDisplay); @@ -86,3 +90,14 @@ void EdhrecApiResponseCardDetailsDisplayWidget::actRequestPageNavigation() { emit requestUrl(toDisplay.url); } + +void EdhrecApiResponseCardDetailsDisplayWidget::applyCardTextFont() +{ + // Family comes from the CardText theme role; the 20px size and bold + // weight previously carried by the stylesheet stay caller-side. Pixel + // size, not point size: the stylesheet said 20px, and 20pt is ~27px. + QFont cardFont = themeManager->appFont(AppFont::CardText); + cardFont.setPixelSize(20); + cardFont.setWeight(QFont::Bold); + nameLabel->setFont(cardFont); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h index d738f4685..8a3c8fba4 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/cards/edhrec_api_response_card_details_display_widget.h @@ -30,6 +30,9 @@ public slots: signals: void requestUrl(QString url); +private slots: + void applyCardTextFont(); + private: EdhrecApiResponseCardDetails toDisplay; QVBoxLayout *layout; diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp index 2ff22f2b3..df0319f88 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.cpp @@ -1,5 +1,6 @@ #include "edhrec_api_response_commander_details_display_widget.h" +#include "../../../../../../theme_manager.h" #include "../../../../../cards/card_info_picture_widget.h" #include "../../../../../cards/card_size_widget.h" #include "../../api_response/cards/edhrec_commander_api_response_commander_details.h" @@ -8,6 +9,7 @@ #include "edhrec_commander_api_response_navigation_widget.h" #include "libcockatrice/card/database/card_database_querier.h" +#include #include #include #include @@ -48,15 +50,16 @@ EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCom commanderName = new QLabel(this); commanderName->setText(commanderDetails.getName()); commanderName->setAlignment(Qt::AlignCenter); - commanderName->setStyleSheet("font-size: 28px; font-weight: bold"); label = new QLabel(this); label->setAlignment(Qt::AlignCenter); - label->setStyleSheet("font-size: 16px"); salt = new QLabel(this); salt->setAlignment(Qt::AlignCenter); - salt->setStyleSheet("font-size: 16px"); + + applyCardTextFont(); + connect(themeManager, &ThemeManager::fontsChanged, this, + &EdhrecCommanderResponseCommanderDetailsDisplayWidget::applyCardTextFont); cardPricesDisplayWidget = new EdhrecApiResponseCardPricesDisplayWidget(this, commanderDetails.getPrices()); @@ -83,3 +86,19 @@ void EdhrecCommanderResponseCommanderDetailsDisplayWidget::retranslateUi() label->setText(commanderDetails.getLabel()); salt->setText(tr("Salt: ") + QString::number(commanderDetails.getSalt())); } + +void EdhrecCommanderResponseCommanderDetailsDisplayWidget::applyCardTextFont() +{ + // Family comes from the CardText theme role; the sizes and the bold + // weight previously carried by the stylesheets stay caller-side. Pixel + // sizes, not point sizes: the stylesheets said 28px and 16px. + QFont nameFont = themeManager->appFont(AppFont::CardText); + nameFont.setPixelSize(28); + nameFont.setWeight(QFont::Bold); + commanderName->setFont(nameFont); + + QFont detailsFont = themeManager->appFont(AppFont::CardText); + detailsFont.setPixelSize(16); + label->setFont(detailsFont); + salt->setFont(detailsFont); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h index 197263512..99e6b3d43 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_api_response_commander_details_display_widget.h @@ -30,6 +30,9 @@ public: QString baseUrl); void retranslateUi(); +private slots: + void applyCardTextFont(); + private: EdhrecCommanderApiResponseCommanderDetails commanderDetails; QHBoxLayout *layout; diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.cpp b/cockatrice/src/interface/widgets/tabs/tab_report.cpp index 988c3e7d9..7986286b3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_report.cpp @@ -1,5 +1,6 @@ #include "tab_report.h" +#include "../interface/theme_manager.h" #include "../server/user/user_context_menu.h" #include "../utility/report_utils.h" #include "abstract_client.h" @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -137,7 +139,8 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T chatGroup = new QGroupBox; chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); - chatLogEdit->setFont(QtUtils::monospaceFont()); + applyChatLogFont(); + connect(themeManager, &ThemeManager::fontsChanged, this, &TabReport::applyChatLogFont); auto *chatLayout = new QVBoxLayout(chatGroup); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); @@ -269,6 +272,11 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T refreshList(); } +void TabReport::applyChatLogFont() +{ + chatLogEdit->setFont(report_utils::reportLogFont()); +} + void TabReport::retranslateUi() { userContextMenu->retranslateUi(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.h b/cockatrice/src/interface/widgets/tabs/tab_report.h index 32ebe1a3f..43e778f4b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.h +++ b/cockatrice/src/interface/widgets/tabs/tab_report.h @@ -62,6 +62,7 @@ private slots: void statsResponse(const Response &response); void reopenReport(); void reopenResponse(const Response &response); + void applyChatLogFont(); private: int selectedReportId() const; diff --git a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp index a629df43c..0090e9d65 100644 --- a/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp +++ b/cockatrice/src/interface/widgets/utility/card_completer_delegate.cpp @@ -1,5 +1,6 @@ #include "card_completer_delegate.h" +#include "../../interface/theme_manager.h" #include "../../pixel_map_generator.h" #include "../cards/additional_info/mana_cost_widget.h" #include "libcockatrice/card/database/card_database_querier.h" @@ -182,7 +183,7 @@ void CardCompleterDelegate::drawManaSymbol(QPainter *p, QPoint centre, const QSt p->setBrush(Qt::NoBrush); p->drawEllipse(pip.adjusted(1, 1, -1, -1)); - QFont f = QtUtils::boldFont(p->font()); + QFont f = QtUtils::boldFont(themeManager->appFont(AppFont::CardText)); f.setPixelSize(qMax(radius - 1, 7)); p->setFont(f); @@ -429,7 +430,7 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem const QRect nameRect = showInfoBand ? QRect(textLeft, rect.top() + 2, textWidth, 20) : QRect(textLeft, rect.top() + 2, textWidth, rect.height() - 4); - QFont f = option.font; + QFont f = themeManager->appFont(AppFont::CardText); f.setPixelSize(13); f.setBold(true); @@ -451,7 +452,7 @@ void CardCompleterDelegate::paint(QPainter *painter, const QStyleOptionViewItem if (showInfoBand) { const QRect infoRect(textLeft, rect.top() + 22, textWidth, rect.height() - 24); - QFont f = option.font; + QFont f = themeManager->appFont(AppFont::CardText); f.setPixelSize(10); painter->setFont(f); diff --git a/cockatrice/src/interface/widgets/utility/report_utils.cpp b/cockatrice/src/interface/widgets/utility/report_utils.cpp index d7fc083c6..df2400b14 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.cpp +++ b/cockatrice/src/interface/widgets/utility/report_utils.cpp @@ -1,5 +1,6 @@ #include "report_utils.h" +#include "../../theme_manager.h" #include "libcockatrice/protocol/pb/serverinfo_report.pb.h" #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +21,20 @@ namespace report_utils { +QFont reportLogFont() +{ + if (themeManager->hasAppFont(AppFont::ChatLog)) { + return themeManager->appFont(AppFont::ChatLog); + } + QFont monoFont(QStringLiteral("monospace")); + monoFont.setStyleHint(QFont::Monospace); + const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); + if (systemPointSize > 0) { + monoFont.setPointSize(systemPointSize); + } + return monoFont; +} + namespace { QColor reportStatusColor(const QString &status) diff --git a/cockatrice/src/interface/widgets/utility/report_utils.h b/cockatrice/src/interface/widgets/utility/report_utils.h index 679989486..32cc19eac 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.h +++ b/cockatrice/src/interface/widgets/utility/report_utils.h @@ -1,6 +1,7 @@ #ifndef REPORT_UTILS_H #define REPORT_UTILS_H +#include #include #include @@ -11,6 +12,12 @@ class ServerInfo_Report; namespace report_utils { +/** @brief Font for the report chat-log views: the theme's ChatLog role when it + * sets one, otherwise the monospace font these views used before the + * role existed. Shared by the three report surfaces so they cannot + * drift apart. */ +QFont reportLogFont(); + QString formatReportCategory(const QString &category); QString formatReportTime(qint64 secondsSinceEpoch); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0bb9654a1..5d956c2c3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -412,6 +412,15 @@ cockatrice_add_test( INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src ) +cockatrice_add_test( + NAME + font_config_test + SOURCES + ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp + font_config_test.cpp + INCLUDE_DIRS + ${CMAKE_SOURCE_DIR}/cockatrice/src +) cockatrice_add_test( NAME loader_local_matching_test diff --git a/tests/font_config_test.cpp b/tests/font_config_test.cpp new file mode 100644 index 000000000..c8b1fb10d --- /dev/null +++ b/tests/font_config_test.cpp @@ -0,0 +1,195 @@ +#include "interface/theme_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +// Writes content to a file inside tmp, creating parents as needed. +QString writeTempFile(QTemporaryDir &tmp, const QString &relativePath, const QString &content) +{ + const QString filePath = QDir(tmp.path()).absoluteFilePath(relativePath); + EXPECT_TRUE(QDir().mkpath(QFileInfo(filePath).absolutePath())); + QFile file(filePath); + EXPECT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text)); + QTextStream out(&file); + out << content; + file.close(); + return filePath; +} + +QFont makeFont(const QString &family, int pointSize) +{ + QFont font(family); + font.setPointSize(pointSize); + return font; +} +} // namespace + +TEST(FontConfigTest, FileNameIsFontsToml) +{ + EXPECT_EQ(FontConfig::fileName(), QStringLiteral("fonts.toml")); +} + +TEST(FontConfigTest, EmptyByDefault) +{ + FontConfig cfg; + EXPECT_FALSE(cfg.hasFonts()); + EXPECT_TRUE(cfg.toToml().isEmpty()); + EXPECT_EQ(cfg, FontConfig()); + EXPECT_EQ(cfg, FontConfig::fromFile("/nonexistent/path/fonts.toml")); + EXPECT_EQ(cfg, FontConfig::fromThemeDir(QString())); +} + +TEST(FontConfigTest, RoundTripPreservesFamilyAndSize) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + FontConfig cfg; + cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); + cfg.fonts[AppFont::ChatLog] = makeFont("JetBrains Mono", 9); + cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11); + cfg.fonts[AppFont::DeckList] = makeFont("Roboto", 12); + ASSERT_TRUE(cfg.hasFonts()); + + const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml()); + const FontConfig loaded = FontConfig::fromFile(filePath); + + EXPECT_EQ(loaded, cfg); + EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].family(), QStringLiteral("Inter")); + EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].pointSize(), 10); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("JetBrains Mono")); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 9); + EXPECT_EQ(loaded.fonts[AppFont::CardText].family(), QStringLiteral("Noto Serif")); + EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 11); + EXPECT_EQ(loaded.fonts[AppFont::DeckList].family(), QStringLiteral("Roboto")); + EXPECT_EQ(loaded.fonts[AppFont::DeckList].pointSize(), 12); +} + +TEST(FontConfigTest, RoundTripStripsWeightAndItalic) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + QFont styled("Inter"); + styled.setPointSize(10); + styled.setBold(true); + styled.setItalic(true); + + FontConfig cfg; + cfg.fonts[AppFont::ChatLog] = styled; + + const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml()); + const FontConfig loaded = FontConfig::fromFile(filePath); + + ASSERT_TRUE(loaded.fonts.contains(AppFont::ChatLog)); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("Inter")); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 10); + EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].bold()); + EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].italic()); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("Inter", 10)); +} + +TEST(FontConfigTest, ToTomlFormat) +{ + FontConfig cfg; + cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); + + const QString toml = cfg.toToml(); + EXPECT_TRUE(toml.contains(QStringLiteral("[Fonts]"))); + EXPECT_TRUE(toml.contains(QStringLiteral("InterfaceDefault"))); + EXPECT_TRUE(toml.contains(QStringLiteral("\"Inter, 10\""))); +} + +TEST(FontConfigTest, MalformedLinesAreSkipped) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + const QString filePath = writeTempFile(tmp, "fonts.toml", + "# a comment\n" + "; another comment\n" + "\n" + "StrayBeforeSection = \"Inter, 10\"\n" + "\n" + "[Other]\n" + "ChatLog = \"ShouldBeIgnored, 10\"\n" + "\n" + "[Fonts]\n" + "InterfaceDefault = \"Inter, 10\"\n" + "ChatLog = \"JetBrains Mono\", 9 # inline comment\n" + "CardText = Noto Serif, 11\n" + "AppFont::DeckList = \"Roboto, 12\"\n" + "UnknownRole = \"Inter, 10\"\n" + "QPalette::Window = \"Inter, 10\"\n" + "NoComma = \"Inter\"\n" + "EmptyFamily = \", 10\"\n" + "ZeroSize = \"Inter, 0\"\n" + "NegativeSize = \"Inter, -3\"\n" + "NotANumber = \"Inter, big\"\n" + "NotAnEntry\n"); + + const FontConfig loaded = FontConfig::fromFile(filePath); + + ASSERT_TRUE(loaded.hasFonts()); + EXPECT_EQ(loaded.fonts.size(), 4); + EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault], makeFont("Inter", 10)); + EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("JetBrains Mono", 9)); + EXPECT_EQ(loaded.fonts[AppFont::CardText], makeFont("Noto Serif", 11)); + EXPECT_EQ(loaded.fonts[AppFont::DeckList], makeFont("Roboto", 12)); +} + +TEST(FontConfigTest, FromThemeDirReadsFontsToml) +{ + QTemporaryDir tmp; + ASSERT_TRUE(tmp.isValid()); + + const QString themeDir = QDir(tmp.path()).absoluteFilePath("MyTheme"); + ASSERT_TRUE(QDir().mkpath(themeDir)); + + FontConfig cfg; + cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11); + writeTempFile(tmp, "MyTheme/fonts.toml", cfg.toToml()); + + const FontConfig loaded = FontConfig::fromThemeDir(themeDir); + EXPECT_EQ(loaded, cfg); + + // A theme directory without fonts.toml loads as empty. + const QString bareDir = QDir(tmp.path()).absoluteFilePath("BareTheme"); + ASSERT_TRUE(QDir().mkpath(bareDir)); + EXPECT_FALSE(FontConfig::fromThemeDir(bareDir).hasFonts()); +} + +TEST(FontConfigTest, InequalityComparesRoles) +{ + FontConfig left; + left.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10); + + FontConfig right = left; + EXPECT_EQ(left, right); + EXPECT_FALSE(left != right); + + right.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 11); + EXPECT_NE(left, right); + + right = left; + right.fonts[AppFont::ChatLog] = makeFont("Inter", 10); + EXPECT_NE(left, right); +} + +// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and +// the fallback then links the bare gtest library without gtest_main. +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}