From f9fe6e0fa534e79ac76223c6fa777bd488dfec52 Mon Sep 17 00:00:00 2001 From: Si Date: Sat, 26 Sep 2026 22:58:44 +0200 Subject: [PATCH] Pick the text stage's font with a font chooser A new Font param type (ParamSpec::makeFont) is a font family as the machine names it. The interface shows it as a family-only FontDialogButton; the command line and the project file treat it as a plain string, as they did the Text param it replaces, so existing projects and --font-family keep working. A family the machine lacks is kept as written. Bumps the version to 0.8.1. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +++++++ CLAUDE.md | 4 +++- CMakeLists.txt | 2 +- packaging/arch/PKGBUILD | 2 +- src/cli/options.cpp | 1 + src/core/params.cpp | 12 ++++++++++++ src/core/params.hpp | 7 ++++++- src/core/project.cpp | 2 ++ src/effects/text.cpp | 2 +- src/gui/window.cpp | 23 ++++++++++++++++++++++ tests/gui_tests.cpp | 43 +++++++++++++++++++++++++++++++++++++++++ 11 files changed, 100 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3d917..f77e195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Every release of Effector, newest first. The version lives in one place, the `project()` line of `CMakeLists.txt`, and each release is a `vX.Y.Z` tag on the commit that made it. The interface shows this file under Changelog in its menu. +## [0.8.1] - 2026-09-26 + +### Added +- The Font of a text stage (and of a text style) is picked from the fonts + installed on this machine with a font chooser, rather than typed by name. + The command line still takes any family, as `--font-family "DejaVu Serif"`. + ## [0.8.0] - 2026-09-26 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 48fb871..2c8f568 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,7 +73,9 @@ GUI or the parser — add a `ParamSpec` instead. A `Path` param names a file on disk; like a source path it is written relative to the project folder. A `Text` param is free-form text the user types, and a `Color` param is a colour written `#rrggbb` (`parseColor`/`formatColor`/`paramColor` in `src/core/params.hpp`), -shown as a colour button in the interface. Values +shown as a colour button in the interface. A `Font` param is a font family +named as the machine names it ("DejaVu Serif"), a family-only font chooser in +the interface and a plain string everywhere else. Values travel as `ParamValues` (a `map>`), read back with `paramInt`/`paramDouble`/`paramBool`/`paramChoice`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f3c171..06589c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(effector VERSION 0.8.0 LANGUAGES CXX) +project(effector VERSION 0.8.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 6f78742..c7d5801 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -2,7 +2,7 @@ # so pkgver must follow the project() line of CMakeLists.txt (the version core # test checks it). From this folder: makepkg -si pkgname=effector -pkgver=0.8.0 +pkgver=0.8.1 pkgrel=1 pkgdesc='Image effects app: a GTK4 interface and a headless CLI over pluggable effects' arch=('x86_64' 'aarch64') diff --git a/src/cli/options.cpp b/src/cli/options.cpp index d9fb6a8..4cf7f48 100644 --- a/src/cli/options.cpp +++ b/src/cli/options.cpp @@ -182,6 +182,7 @@ Options parseOptions(int argc, char** argv) { } case ParamSpec::Type::Path: case ParamSpec::Type::Text: + case ParamSpec::Type::Font: options.params[spec->key] = requireValue(argc, argv, i, arg); break; case ParamSpec::Type::Color: { diff --git a/src/core/params.cpp b/src/core/params.cpp index fc95c52..3a7d01d 100644 --- a/src/core/params.cpp +++ b/src/core/params.cpp @@ -100,6 +100,17 @@ ParamSpec ParamSpec::makeColor(std::string key, std::string label, std::string d return spec; } +ParamSpec ParamSpec::makeFont(std::string key, std::string label, std::string defaultFamily, + std::string help) { + ParamSpec spec; + spec.type = Type::Font; + spec.key = std::move(key); + spec.label = std::move(label); + spec.help = std::move(help); + spec.defaultChoice = std::move(defaultFamily); + return spec; +} + namespace { const ParamValue* find(const ParamValues& values, const std::string& key) { @@ -121,6 +132,7 @@ ParamValue paramDefault(const ParamSpec& spec) { case ParamSpec::Type::Path: case ParamSpec::Type::Text: case ParamSpec::Type::Color: + case ParamSpec::Type::Font: break; } return spec.defaultChoice; diff --git a/src/core/params.hpp b/src/core/params.hpp index a834cc5..ece833e 100644 --- a/src/core/params.hpp +++ b/src/core/params.hpp @@ -11,7 +11,7 @@ namespace effector { // Declarative description of one effect knob. The GUI turns these into widgets // and the CLI turns them into flags, so neither knows about any effect. struct ParamSpec { - enum class Type { Int, Double, Bool, Choice, Path, Text, Color }; + enum class Type { Int, Double, Bool, Choice, Path, Text, Color, Font }; Type type = Type::Int; std::string key; @@ -56,6 +56,11 @@ struct ParamSpec { // spelling as a flag value. static ParamSpec makeColor(std::string key, std::string label, std::string defaultColor, std::string help = {}); + // A font family as it is named on this machine ("Sans", "DejaVu Serif"): + // a font chooser in the interface, a flag taking any string on the + // command line. A family the machine lacks is kept as written. + static ParamSpec makeFont(std::string key, std::string label, std::string defaultFamily, + std::string help = {}); }; using ParamValue = std::variant; diff --git a/src/core/project.cpp b/src/core/project.cpp index bb1e266..222f2e0 100644 --- a/src/core/project.cpp +++ b/src/core/project.cpp @@ -366,6 +366,7 @@ void writeParams(std::ostream& out, const std::vector& specs, const P break; case ParamSpec::Type::Choice: case ParamSpec::Type::Text: + case ParamSpec::Type::Font: out << quote(paramText(values, spec.key, spec.defaultChoice)); break; case ParamSpec::Type::Color: { @@ -412,6 +413,7 @@ void readParams(const std::vector& specs, const Json& params, } break; case ParamSpec::Type::Text: + case ParamSpec::Type::Font: if (value->kind == Json::Kind::String) values[spec.key] = value->text; break; case ParamSpec::Type::Color: { diff --git a/src/effects/text.cpp b/src/effects/text.cpp index 722fd2d..f3fee00 100644 --- a/src/effects/text.cpp +++ b/src/effects/text.cpp @@ -16,7 +16,7 @@ const std::vector& TextEffect::params() const { static const std::vector specs = { ParamSpec::makeText("text", "Text", "Effector", "What to write; a line break starts a new line."), - ParamSpec::makeText("font_family", "Font", "Sans", + ParamSpec::makeFont("font_family", "Font", "Sans", "Font family, as it is named on this machine: Sans, Serif, Monospace, " "\"DejaVu Serif\", ...") .styled(), diff --git a/src/gui/window.cpp b/src/gui/window.cpp index 3563a26..0e805e4 100644 --- a/src/gui/window.cpp +++ b/src/gui/window.cpp @@ -995,6 +995,21 @@ void EffectorWindow::buildParamWidgets() { widget = std::move(button); break; } + case ParamSpec::Type::Font: { + // Only the family is chosen here: weight, slant and size are + // params of their own. + auto button = std::make_unique(Gtk::FontDialog::create()); + button->set_halign(Gtk::Align::START); + button->set_level(Gtk::FontLevel::FAMILY); + button->set_use_font(true); + Pango::FontDescription font; + font.set_family(paramText(values, spec.key, spec.defaultChoice)); + button->set_font_desc(font); + button->property_font_desc().signal_changed().connect( + sigc::mem_fun(*this, &EffectorWindow::onParamChanged)); + widget = std::move(button); + break; + } case ParamSpec::Type::Choice: { std::vector choices(spec.choices.begin(), spec.choices.end()); auto dropdown = std::make_unique(); @@ -1051,6 +1066,14 @@ ParamValues EffectorWindow::paramValues() const { values[entry.spec.key] = formatColor(rgb); break; } + case ParamSpec::Type::Font: { + // A family the machine lacks keeps its name, so a project + // from elsewhere still says which font it wanted. + const auto family = + static_cast(entry.widget)->get_font_desc().get_family(); + if (!family.empty()) values[entry.spec.key] = std::string(family); + break; + } case ParamSpec::Type::Choice: { const auto index = static_cast(entry.widget)->get_selected(); if (index < entry.spec.choices.size()) values[entry.spec.key] = entry.spec.choices[index]; diff --git a/tests/gui_tests.cpp b/tests/gui_tests.cpp index e6976b5..a085e65 100644 --- a/tests/gui_tests.cpp +++ b/tests/gui_tests.cpp @@ -591,6 +591,48 @@ void testVideo(Gtk::Window& window, const std::filesystem::path& dir) { stack->set_visible_child("chain"); } +// A font param is a font chooser showing the family, and the family picked +// there is what the stage renders with and what the project saves. +void testFont(Gtk::Window& window, const std::filesystem::path& dir) { + std::cout << "font\n"; + std::filesystem::create_directories(dir); + const auto path = dir / "lettering.json"; + effector::Project project; + effector::ChainStage stage{effector::effectByName("text"), {}}; + stage.values["font_family"] = std::string("Serif"); + project.stages.push_back(stage); + effector::saveProject(path.string(), project); + window.activate_action("win.open-recent", + Glib::Variant::create(Glib::filename_to_uri(path.string()))); + pump(300); + + auto* chainList = named(window, "chain-list"); + if (chainList != nullptr && chainList->get_row_at_index(0) != nullptr) { + chainList->select_row(*chainList->get_row_at_index(0)); + } + pump(100); + std::vector buttons; + collect(window, [](Gtk::FontDialogButton& b) { return b.is_drawable(); }, + buttons); + check(buttons.size() == 1, "the text stage picks its font with a font chooser"); + if (buttons.size() != 1) return; + auto* button = buttons.front(); + check(button->get_level() == Gtk::FontLevel::FAMILY, + "which chooses only the family, the weight and size being params of their own"); + check(button->get_font_desc().get_family() == "Serif", "and shows the family saved"); + + Pango::FontDescription mono; + mono.set_family("Monospace"); + button->set_font_desc(mono); + pump(100); + window.activate_action("win.save-project"); + pump(100); + const auto saved = effector::loadProject(path.string()); + check(saved.stages.size() == 1 && + effector::paramText(saved.stages[0].values, "font_family", "") == "Monospace", + "a family chosen there is saved with the project"); +} + // A project whose images will not fit is refused before any is decoded, and // saving it then must not quietly drop them from the file. void testMemoryGuard(Gtk::Window& window, const std::filesystem::path& dir) { @@ -658,6 +700,7 @@ int main(int argc, char** argv) { testHelpWindows(*window); testRecentProjects(*window, dataDir / "projects"); testVideo(*window, dataDir / "video"); + testFont(*window, dataDir / "font"); testMemoryGuard(*window, dataDir / "memory"); app->remove_window(*window); });