[Fonts] Retire max font size setting into theme CardText min/max range

The per-card text size lives in the theme now, so a theme can pick the size
it was designed for instead of every user overriding it globally.

- CardText stores family + max point size; CardTextMinSize stores the
  overlay shrink-to-fit floor (omitted from fonts.toml when 1).
- ThemeManager::cardTextMaxSize()/cardTextMinSize() resolve the range with
  the retired defaults (12 max, 1 min), and cardTextMaxSize() falls back to
  the CardText role size that #7541 made live.
- The overlay widget starts at the explicit size when one is set (deck
  preview), else the theme max, and shrinks down to the theme floor.
- The appearance page's "Maximum font size" spin is gone; the range is
  edited in the theme's font grid, whose min/max spins now keep the saved
  range consistent with each other.
- Migration: appearance/maxFontSize is seeded into every user theme folder
  and into the active theme's folder (created when the theme is a shipped
  one that only exists in the system directory). The retired
  game/maxfontsize -> appearance/maxFontSize mapping stays, so users still
  on global.ini keep their value through the chain.
This commit is contained in:
Lukas Brübach 2026-10-16 11:27:31 +02:00
parent b01fce8037
commit e50b928f8b
19 changed files with 697 additions and 49 deletions

View file

@ -137,6 +137,9 @@ SettingsCache::SettingsCache()
// before that rename (idempotent, safe to run on every startup, portable builds included).
SettingsMigration::migrateGameFiltersToCamelCase(settingsPath);
// Retire appearance/maxFontSize into per-theme CardText max (idempotent, safe every startup).
SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath);
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
serversSettings = new ServersSettings(settingsPath, this);

View file

@ -36,7 +36,6 @@
#include <QtGlobal>
#include <algorithm>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/debug_settings.h>
#include <qnamespace.h>
@ -136,8 +135,8 @@ QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const
void AbstractCardItem::transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle)
{
const int MAX_FONT_SIZE = SettingsCache::instance().appearance().getMaxFontSize();
const int fontSize = std::max(9, MAX_FONT_SIZE);
const int minSize = std::max(1, themeManager->cardTextMinSize());
const int fontSize = std::max(minSize, std::max(1, themeManager->cardTextMaxSize()));
QRectF totalBoundingRect = painter->combinedTransform().mapRect(boundingRect());

View file

@ -13,6 +13,8 @@
static constexpr int MIN_FONT_SIZE = 6;
static constexpr int MAX_FONT_SIZE = 72;
static constexpr int CARD_TEXT_MIN_SIZE = 1;
static constexpr int CARD_TEXT_MAX_SIZE = 12;
static constexpr int FALLBACK_FONT_SIZE = 10;
static const QMap<AppFont::Role, const char *> APP_FONT_DESCRIPTIONS = {
@ -46,6 +48,7 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
grid->setColumnStretch(0, 0);
grid->setColumnStretch(1, 1);
grid->setColumnStretch(2, 0);
grid->setColumnStretch(3, 0);
QMetaEnum fontEnum = QMetaEnum::fromType<AppFont::Role>();
for (int i = 0; i < fontEnum.keyCount(); ++i) {
@ -58,6 +61,30 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
grid->addWidget(familyCombo, i, 1);
connect(familyCombo, &QFontComboBox::currentFontChanged, this, [this] { emit fontsChanged(); });
if (role == AppFont::CardText) {
auto *minSpin = new QSpinBox(this);
minSpin->setRange(CARD_TEXT_MIN_SIZE, MAX_FONT_SIZE);
grid->addWidget(minSpin, i, 2);
connect(minSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
auto *maxSpin = new QSpinBox(this);
maxSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE);
grid->addWidget(maxSpin, i, 3);
connect(maxSpin, &QSpinBox::valueChanged, this, [this] { emit fontsChanged(); });
// Keep the range consistent: otherwise min 20 / max 12 saves as-is
// and cardTextMaxSize() silently clamps the max up to the min, so
// the max spin shows a size that is never used. Moving one spin
// pulls the other's bound along.
connect(minSpin, &QSpinBox::valueChanged, this, [maxSpin](int value) { maxSpin->setMinimum(value); });
connect(maxSpin, &QSpinBox::valueChanged, this, [minSpin](int value) { minSpin->setMaximum(value); });
label->setBuddy(familyCombo);
fontRows[role] = {label, familyCombo, maxSpin, minSpin};
continue;
}
auto *sizeSpin = new QSpinBox(this);
sizeSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE);
grid->addWidget(sizeSpin, i, 2);
@ -65,7 +92,7 @@ FontGridWidget::FontGridWidget(QWidget *parent) : QWidget(parent)
label->setBuddy(familyCombo);
fontRows[role] = {label, familyCombo, sizeSpin};
fontRows[role] = {label, familyCombo, sizeSpin, nullptr};
}
retranslateUi();
@ -89,6 +116,37 @@ void FontGridWidget::loadFonts(const FontConfig &cfg)
const QFont font = cfg.fonts.value(role, fallback);
row.familyCombo->setCurrentFont(font);
if (role == AppFont::CardText && row.minSpin != nullptr) {
// A previous load may have narrowed the linked bounds (min 20 pulls
// max up to 20), which would clamp the values written below. Reset
// both to the full range first; the valueChanged lambdas re-link
// them as the values are set.
row.minSpin->setRange(CARD_TEXT_MIN_SIZE, MAX_FONT_SIZE);
row.sizeSpin->setRange(MIN_FONT_SIZE, MAX_FONT_SIZE);
// The max spin is a size-only override. Without CardTextMaxSize it
// follows the CardText family size, and without CardText at all it
// must match cardTextMaxSize()'s default rather than the system
// startup size, which currentFontConfig() would otherwise bake in.
const int fallbackMax = cfg.fonts.contains(role) ? font.pointSize() : CARD_TEXT_MAX_SIZE;
int maxSize = cfg.cardTextMaxSize > 0 ? cfg.cardTextMaxSize : fallbackMax;
if (maxSize <= 0) {
maxSize = CARD_TEXT_MAX_SIZE;
}
int minSize = cfg.cardTextMinSize > 0 ? cfg.cardTextMinSize : themeManager->cardTextMinSize();
minSize = qBound(CARD_TEXT_MIN_SIZE, minSize, MAX_FONT_SIZE);
maxSize = qBound(MIN_FONT_SIZE, maxSize, MAX_FONT_SIZE);
if (maxSize < minSize) {
maxSize = minSize;
}
// Max first, then min: each value re-applies the linked bound, so
// the pair ends up consistent instead of clamping the smaller one.
row.sizeSpin->setValue(maxSize);
row.minSpin->setValue(minSize);
continue;
}
int size = font.pointSize();
if (size <= 0) {
size = qApp->font().pointSize();
@ -114,6 +172,11 @@ FontConfig FontGridWidget::currentFontConfig() const
QFont font(row.familyCombo->currentFont().family());
font.setPointSize(row.sizeSpin->value());
cfg.fonts[role] = font;
if (role == AppFont::CardText && row.minSpin != nullptr) {
cfg.cardTextMinSize = row.minSpin->value();
cfg.cardTextMaxSize = row.sizeSpin->value();
}
}
return cfg;
}
@ -129,6 +192,15 @@ void FontGridWidget::retranslateUi()
row.label->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
row.familyCombo->setToolTip(tr(APP_FONT_DESCRIPTIONS.value(role, "")));
row.familyCombo->setAccessibleName(roleDisplayName(role));
if (role == AppFont::CardText && row.minSpin != nullptr) {
row.minSpin->setToolTip(tr("Minimum card text size"));
row.minSpin->setAccessibleName(tr("Minimum card text size"));
row.minSpin->setSuffix(tr(" pt"));
row.sizeSpin->setToolTip(tr("Maximum card text size"));
row.sizeSpin->setAccessibleName(tr("Maximum card text size"));
row.sizeSpin->setSuffix(tr(" pt"));
continue;
}
row.sizeSpin->setToolTip(tr("Point size for %1").arg(roleDisplayName(role)));
row.sizeSpin->setAccessibleName(tr("Point size for %1").arg(roleDisplayName(role)));
row.sizeSpin->setSuffix(tr(" pt"));

View file

@ -35,6 +35,7 @@ private:
QLabel *label = nullptr;
QFontComboBox *familyCombo = nullptr;
QSpinBox *sizeSpin = nullptr;
QSpinBox *minSpin = nullptr; // CardText only: shrink-to-fit floor
};
QMap<AppFont::Role, FontRow> fontRows;

View file

@ -308,7 +308,7 @@ QPalette PaletteConfig::apply(QPalette base) const
bool FontConfig::hasFonts() const
{
return !fonts.isEmpty();
return !fonts.isEmpty() || cardTextMinSize != 1 || cardTextMaxSize > 0;
}
QString FontConfig::toToml() const
@ -334,6 +334,14 @@ QString FontConfig::toToml() const
entries += QString("%1 = \"%2, %3\"\n").arg(QString(roleName), -20).arg(family).arg(pointSize);
}
if (cardTextMinSize != 1) {
entries += QString("%1 = %2\n").arg(QStringLiteral("CardTextMinSize"), -20).arg(cardTextMinSize);
}
if (cardTextMaxSize > 0) {
entries += QString("%1 = %2\n").arg(QStringLiteral("CardTextMaxSize"), -20).arg(cardTextMaxSize);
}
if (entries.isEmpty()) {
return {};
}
@ -401,6 +409,31 @@ FontConfig FontConfig::fromFile(const QString &filePath)
value = value.mid(1, value.size() - 2).trimmed();
}
// CardText size bounds: bare integers, e.g. CardTextMinSize = 2.
// Tolerate quoting like the family entries; skip invalid lines.
QString sizeKey = key;
if (sizeKey.startsWith("AppFont::")) {
sizeKey = sizeKey.mid(QStringLiteral("AppFont::").size());
}
if (sizeKey.compare("CardTextMinSize", Qt::CaseInsensitive) == 0) {
bool ok = false;
int minSize = value.trimmed().toInt(&ok);
if (!ok || minSize <= 0) {
continue;
}
cfg.cardTextMinSize = minSize;
continue;
}
if (sizeKey.compare("CardTextMaxSize", Qt::CaseInsensitive) == 0) {
bool ok = false;
int maxSize = value.trimmed().toInt(&ok);
if (!ok || maxSize <= 0) {
continue;
}
cfg.cardTextMaxSize = maxSize;
continue;
}
int comma = value.lastIndexOf(',');
if (comma < 0) {

View file

@ -84,15 +84,23 @@ struct PaletteConfig
* @code
* [Fonts]
* InterfaceDefault = "Inter, 10"
* CardText = "Noto Serif, 12"
* CardTextMinSize = 1
* @endcode
* CardText stores family + MAX size; cardTextMinSize stores the overlay
* shrink-to-fit floor (omitted from TOML when 1).
*/
struct FontConfig
{
QMap<AppFont::Role, QFont> fonts;
int cardTextMinSize = 1;
/// Optional size-only maximum for CardText, used by the retired maxFontSize
/// migration so seeding it does not have to pin a family.
int cardTextMaxSize = 0;
bool operator==(const FontConfig &rhs) const
{
return fonts == rhs.fonts;
return fonts == rhs.fonts && cardTextMinSize == rhs.cardTextMinSize && cardTextMaxSize == rhs.cardTextMaxSize;
}
bool operator!=(const FontConfig &rhs) const
{

View file

@ -678,6 +678,7 @@ QFont ThemeManager::cardTextFont() const
}
QFont serifFont(QStringLiteral("Serif"));
serifFont.setStyleHint(QFont::Serif);
serifFont.setPointSize(12);
return serifFont;
}
@ -699,6 +700,29 @@ QFont ThemeManager::defaultAppFont(AppFont::Role /*role*/) const
return defaultFont;
}
int ThemeManager::cardTextMaxSize() const
{
if (currentFonts.cardTextMaxSize > 0) {
return currentFonts.cardTextMaxSize;
}
const auto it = currentFonts.fonts.constFind(AppFont::CardText);
if (it != currentFonts.fonts.constEnd()) {
const int size = it.value().pointSize();
if (size > 0) {
return size;
}
}
return 12;
}
int ThemeManager::cardTextMinSize() const
{
if (currentFonts.cardTextMinSize > 0) {
return currentFonts.cardTextMinSize;
}
return 1;
}
void ThemeManager::themeChangedSlot()
{
QString themeName = SettingsCache::instance().getThemeName();

View file

@ -169,6 +169,11 @@ public:
* "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;
/** @brief CardText max size: stored CardTextMaxSize, else the CardText
* point size, else 12. */
int cardTextMaxSize() const;
/** @brief CardText shrink-to-fit floor: stored CardTextMinSize, else 1. */
int cardTextMinSize() const;
QBrush &getBgBrush(Role zone);
QBrush getExtraBgBrush(Role zone, int zoneId = 0);

View file

@ -87,6 +87,7 @@ void CardInfoPictureWithTextOverlayWidget::setOutlineColor(const QColor &color)
void CardInfoPictureWithTextOverlayWidget::setFontSize(const int size)
{
fontSize = size > 0 ? size : 1;
hasExplicitFontSize = true;
update();
}
@ -179,10 +180,12 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
}
// 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.
// theme role; the start size is the explicit size when set (e.g. deck
// preview 24), else the theme max, shrinking down to the theme min floor.
QFont font = themeManager->appFont(AppFont::CardText);
int optimalFontSize = fontSize; // Start with the user-defined font size
const int themeMin = std::max(1, themeManager->cardTextMinSize());
const int themeMax = std::max(themeMin, std::max(1, themeManager->cardTextMaxSize()));
int optimalFontSize = hasExplicitFontSize ? std::max(1, fontSize) : themeMax;
const QFontMetrics baseMetrics(font);
int textWidth = pixmapRect.width();
@ -200,7 +203,7 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
}
--optimalFontSize;
} while (optimalFontSize > 1);
} while (optimalFontSize > themeMin);
// Apply the calculated font size
painter.setFont(font);
@ -227,7 +230,7 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
int totalTextHeight = wrappedText.count('\n') * fontMetrics.height() + fontMetrics.height();
// Adjust font size if the total text height exceeds the pixmap height
while (totalTextHeight > pixmapRect.height() && optimalFontSize > 1) {
while (totalTextHeight > pixmapRect.height() && optimalFontSize > themeMin) {
--optimalFontSize;
font.setPointSize(optimalFontSize);
painter.setFont(font);
@ -297,7 +300,9 @@ QSize CardInfoPictureWithTextOverlayWidget::minimumSizeHint() const
// Get the font metrics for the overlay text
QFont font = themeManager->appFont(AppFont::CardText);
font.setPointSize(fontSize);
const int themeMin = std::max(1, themeManager->cardTextMinSize());
const int themeMax = std::max(themeMin, std::max(1, themeManager->cardTextMaxSize()));
font.setPointSize(hasExplicitFontSize ? std::max(1, fontSize) : themeMax);
const QFontMetrics fontMetrics(font);
// Calculate the height required for the text

View file

@ -60,6 +60,7 @@ private:
QColor textColor;
QColor outlineColor;
int fontSize;
bool hasExplicitFontSize = false;
Qt::Alignment textAlignment;
bool highlighted;
};

View file

@ -284,20 +284,11 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&roundCardCornersCheckBox, &QAbstractButton::toggled, &settings.cardsDisplay(),
&CardsDisplaySettings::setRoundCardCorners);
connect(&maxFontSizeForCardsEdit, qOverload<int>(&QSpinBox::valueChanged), &settings.appearance(),
&AppearanceSettings::setMaxFontSize);
maxFontSizeForCardsEdit.setValue(settings.appearance().getMaxFontSize());
maxFontSizeForCardsLabel.setBuddy(&maxFontSizeForCardsEdit);
maxFontSizeForCardsEdit.setMinimum(9);
maxFontSizeForCardsEdit.setMaximum(100);
auto *cardsGrid = new QGridLayout;
cardsGrid->addWidget(&displayCardNamesCheckBox, 0, 0, 1, 2);
cardsGrid->addWidget(&autoRotateSidewaysLayoutCardsCheckBox, 1, 0, 1, 2);
cardsGrid->addWidget(&cardScalingCheckBox, 2, 0, 1, 2);
cardsGrid->addWidget(&roundCardCornersCheckBox, 3, 0, 1, 2);
cardsGrid->addWidget(&maxFontSizeForCardsLabel, 4, 0, 1, 1);
cardsGrid->addWidget(&maxFontSizeForCardsEdit, 4, 1, 1, 1);
cardsGroupBox = new QGroupBox;
cardsGroupBox->setLayout(cardsGrid);
@ -658,7 +649,6 @@ void AppearanceSettingsPage::retranslateUi()
autoRotateSidewaysLayoutCardsCheckBox.setText(tr("Auto-Rotate cards with sideways layout"));
cardScalingCheckBox.setText(tr("Scale cards on mouse over"));
roundCardCornersCheckBox.setText(tr("Use rounded card corners"));
maxFontSizeForCardsLabel.setText(tr("Maximum font size for information displayed on cards:"));
cardLayoutGroupBox->setTitle(tr("Card layout"));
verticalCardOverlapPercentLabel.setText(

View file

@ -71,8 +71,6 @@ private:
QCheckBox autoRotateSidewaysLayoutCardsCheckBox;
QCheckBox cardScalingCheckBox;
QCheckBox roundCardCornersCheckBox;
QLabel maxFontSizeForCardsLabel;
QSpinBox maxFontSizeForCardsEdit;
QLabel verticalCardOverlapPercentLabel;
QSpinBox verticalCardOverlapPercentBox;