[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 • committed by BruebachL
parent ed119cc940
commit e87cd80c9b
19 changed files with 697 additions and 49 deletions

View file

@ -10,6 +10,7 @@
#include <QTemporaryDir>
#include <QTextStream>
#include <gtest/gtest.h>
#include <string>
namespace
{
@ -186,6 +187,154 @@ TEST(FontConfigTest, InequalityComparesRoles)
EXPECT_NE(left, right);
}
TEST(FontConfigTest, CardTextMinRoundTrip)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 14);
cfg.cardTextMinSize = 3;
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::CardText].pointSize(), 14);
EXPECT_EQ(loaded.cardTextMinSize, 3);
}
TEST(FontConfigTest, CardTextMinDefaultIsOmitted)
{
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
EXPECT_EQ(cfg.cardTextMinSize, 1);
const QString toml = cfg.toToml();
EXPECT_TRUE(toml.contains(QStringLiteral("CardText")));
EXPECT_FALSE(toml.contains(QStringLiteral("CardTextMinSize")));
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml", toml);
EXPECT_EQ(FontConfig::fromFile(filePath).cardTextMinSize, 1);
}
TEST(FontConfigTest, MalformedMinIsSkipped)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml",
"[Fonts]\n"
"CardText = \"Noto Serif, 14\"\n"
"CardTextMinSize = big\n");
// Each malformed value parses independently, so rewrite the file per case.
const QStringList badValues = {"big", "0", "-3", "", "Inter, 10"};
for (const QString &bad : badValues) {
const QString badPath = writeTempFile(
tmp, "bad.toml", QString("[Fonts]\nCardText = \"Noto Serif, 14\"\nCardTextMinSize = %1\n").arg(bad));
const FontConfig loaded = FontConfig::fromFile(badPath);
EXPECT_EQ(loaded.cardTextMinSize, 1) << "bad value: " << bad.toStdString();
ASSERT_TRUE(loaded.fonts.contains(AppFont::CardText));
EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 14);
}
// The valid file still loads.
EXPECT_EQ(FontConfig::fromFile(filePath).cardTextMinSize, 1);
}
TEST(FontConfigTest, CardTextMaxRoundTrip)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 14);
cfg.cardTextMaxSize = 18;
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.cardTextMaxSize, 18);
EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 14);
}
TEST(FontConfigTest, CardTextMaxSizeWithoutFamily)
{
// The retired maxFontSize setting migrates to a size-only key, so the
// family keeps its existing resolution instead of being pinned to Serif.
FontConfig cfg;
cfg.cardTextMaxSize = 18;
ASSERT_TRUE(cfg.hasFonts());
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml());
const FontConfig loaded = FontConfig::fromFile(filePath);
EXPECT_TRUE(loaded.fonts.isEmpty());
EXPECT_EQ(loaded.cardTextMaxSize, 18);
EXPECT_EQ(loaded, cfg);
}
TEST(FontConfigTest, CardTextMaxDefaultIsOmitted)
{
FontConfig cfg;
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
EXPECT_EQ(cfg.cardTextMaxSize, 0);
const QString toml = cfg.toToml();
EXPECT_FALSE(toml.contains(QStringLiteral("CardTextMaxSize")));
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QString filePath = writeTempFile(tmp, "fonts.toml", toml);
EXPECT_EQ(FontConfig::fromFile(filePath).cardTextMaxSize, 0);
}
TEST(FontConfigTest, MalformedMaxIsSkipped)
{
QTemporaryDir tmp;
ASSERT_TRUE(tmp.isValid());
const QStringList badValues = {"big", "0", "-3", "", "Inter, 18"};
for (const QString &bad : badValues) {
const QString badPath = writeTempFile(
tmp, "bad.toml", QString("[Fonts]\nCardText = \"Noto Serif, 14\"\nCardTextMaxSize = %1\n").arg(bad));
const FontConfig loaded = FontConfig::fromFile(badPath);
EXPECT_EQ(loaded.cardTextMaxSize, 0) << "bad value: " << bad.toStdString();
ASSERT_TRUE(loaded.fonts.contains(AppFont::CardText));
EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 14);
}
}
TEST(FontConfigTest, InequalityComparesMinSize)
{
FontConfig left;
left.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
left.cardTextMinSize = 1;
FontConfig right = left;
right.cardTextMinSize = 2;
EXPECT_NE(left, right);
}
TEST(FontConfigTest, InequalityComparesMaxSize)
{
FontConfig left;
left.fonts[AppFont::CardText] = makeFont("Noto Serif", 12);
left.cardTextMaxSize = 12;
FontConfig right = left;
right.cardTextMaxSize = 18;
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)

View file

@ -461,19 +461,6 @@ TEST_F(SettingsDefaultsTest, Appearance_StyleUserList_Default)
ASSERT_EQ(s.getStyleUserList(), true);
}
TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_Default)
{
AppearanceSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getMaxFontSize(), 12);
}
TEST_F(SettingsDefaultsTest, Appearance_MaxFontSize_SetAndGet)
{
AppearanceSettings s(settingsPath, nullptr);
s.setMaxFontSize(14);
ASSERT_EQ(s.getMaxFontSize(), 14);
}
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundSource_Default)
{
AppearanceSettings s(settingsPath, nullptr);

View file

@ -984,6 +984,198 @@ TEST_F(SettingsMigrationTest, IsNotMigrationPendingWithoutLegacyData)
ASSERT_FALSE(SettingsMigration::isMigrationPending(settingsPath));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_NonDefaultSeedsThemeAndDeletesKey)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
const QString fontsPath = themesPath + "/MyTheme/fonts.toml";
ASSERT_TRUE(QFile::exists(fontsPath));
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
// Size-only: the migration must not pin the family, or every CardText
// surface switches to Serif for users who never chose it.
EXPECT_FALSE(content.contains("Serif"));
ASSERT_FALSE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_SeedsActiveStockTheme)
{
// A stock theme resolves to the system directory, so there is no user theme
// folder to seed. The migration has to create the active theme's folder,
// otherwise the retired size is deleted without landing anywhere.
const QString themesPath = tempDir.path() + "/themes";
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/themeName", "Fabric");
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
const QString fontsPath = themesPath + "/Fabric/fonts.toml";
ASSERT_TRUE(QFile::exists(fontsPath));
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_KeepsExistingActiveTheme)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/Fabric"));
{
QFile f(themesPath + "/Fabric/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nCardText = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/themeName", "Fabric");
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/Fabric/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("Inter, 10"));
EXPECT_FALSE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_DefaultDeletesKeyWithoutFiles)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 12);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
ASSERT_FALSE(QFile::exists(themesPath + "/MyTheme/fonts.toml"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_DoesNotOverwriteExplicitMax)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nCardText = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("10"));
EXPECT_FALSE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_MergesWithOtherRoles)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nInterfaceDefault = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 20);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("InterfaceDefault"));
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("20"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_AbsentKeyDoesNothing)
{
ASSERT_FALSE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
}
TEST_F(SettingsMigrationTest, IsMigrationPendingWhenMaxFontSizePresent)
{
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::isMigrationPending(settingsPath));
}
} // namespace
int main(int argc, char **argv)