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 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-26 22:58:44 +02:00
co-authored by Claude Opus 5.5
parent 8acf85ddb8
commit f9fe6e0fa5
11 changed files with 100 additions and 5 deletions
+7
View File
@@ -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 `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. 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 ## [0.8.0] - 2026-09-26
### Added ### Added
+3 -1
View File
@@ -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` 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 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`), `#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 travel as `ParamValues` (a
`map<string, variant<long,double,bool,string>>`), read back with `map<string, variant<long,double,bool,string>>`), read back with
`paramInt`/`paramDouble`/`paramBool`/`paramChoice`. `paramInt`/`paramDouble`/`paramBool`/`paramChoice`.
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20) 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 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
+1 -1
View File
@@ -2,7 +2,7 @@
# so pkgver must follow the project() line of CMakeLists.txt (the version core # so pkgver must follow the project() line of CMakeLists.txt (the version core
# test checks it). From this folder: makepkg -si # test checks it). From this folder: makepkg -si
pkgname=effector pkgname=effector
pkgver=0.8.0 pkgver=0.8.1
pkgrel=1 pkgrel=1
pkgdesc='Image effects app: a GTK4 interface and a headless CLI over pluggable effects' pkgdesc='Image effects app: a GTK4 interface and a headless CLI over pluggable effects'
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
+1
View File
@@ -182,6 +182,7 @@ Options parseOptions(int argc, char** argv) {
} }
case ParamSpec::Type::Path: case ParamSpec::Type::Path:
case ParamSpec::Type::Text: case ParamSpec::Type::Text:
case ParamSpec::Type::Font:
options.params[spec->key] = requireValue(argc, argv, i, arg); options.params[spec->key] = requireValue(argc, argv, i, arg);
break; break;
case ParamSpec::Type::Color: { case ParamSpec::Type::Color: {
+12
View File
@@ -100,6 +100,17 @@ ParamSpec ParamSpec::makeColor(std::string key, std::string label, std::string d
return spec; 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 { namespace {
const ParamValue* find(const ParamValues& values, const std::string& key) { 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::Path:
case ParamSpec::Type::Text: case ParamSpec::Type::Text:
case ParamSpec::Type::Color: case ParamSpec::Type::Color:
case ParamSpec::Type::Font:
break; break;
} }
return spec.defaultChoice; return spec.defaultChoice;
+6 -1
View File
@@ -11,7 +11,7 @@ namespace effector {
// Declarative description of one effect knob. The GUI turns these into widgets // 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. // and the CLI turns them into flags, so neither knows about any effect.
struct ParamSpec { 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; Type type = Type::Int;
std::string key; std::string key;
@@ -56,6 +56,11 @@ struct ParamSpec {
// spelling as a flag value. // spelling as a flag value.
static ParamSpec makeColor(std::string key, std::string label, std::string defaultColor, static ParamSpec makeColor(std::string key, std::string label, std::string defaultColor,
std::string help = {}); 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<long, double, bool, std::string>; using ParamValue = std::variant<long, double, bool, std::string>;
+2
View File
@@ -366,6 +366,7 @@ void writeParams(std::ostream& out, const std::vector<ParamSpec>& specs, const P
break; break;
case ParamSpec::Type::Choice: case ParamSpec::Type::Choice:
case ParamSpec::Type::Text: case ParamSpec::Type::Text:
case ParamSpec::Type::Font:
out << quote(paramText(values, spec.key, spec.defaultChoice)); out << quote(paramText(values, spec.key, spec.defaultChoice));
break; break;
case ParamSpec::Type::Color: { case ParamSpec::Type::Color: {
@@ -412,6 +413,7 @@ void readParams(const std::vector<ParamSpec>& specs, const Json& params,
} }
break; break;
case ParamSpec::Type::Text: case ParamSpec::Type::Text:
case ParamSpec::Type::Font:
if (value->kind == Json::Kind::String) values[spec.key] = value->text; if (value->kind == Json::Kind::String) values[spec.key] = value->text;
break; break;
case ParamSpec::Type::Color: { case ParamSpec::Type::Color: {
+1 -1
View File
@@ -16,7 +16,7 @@ const std::vector<ParamSpec>& TextEffect::params() const {
static const std::vector<ParamSpec> specs = { static const std::vector<ParamSpec> specs = {
ParamSpec::makeText("text", "Text", "Effector", ParamSpec::makeText("text", "Text", "Effector",
"What to write; a line break starts a new line."), "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, " "Font family, as it is named on this machine: Sans, Serif, Monospace, "
"\"DejaVu Serif\", ...") "\"DejaVu Serif\", ...")
.styled(), .styled(),
+23
View File
@@ -995,6 +995,21 @@ void EffectorWindow::buildParamWidgets() {
widget = std::move(button); widget = std::move(button);
break; 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::FontDialogButton>(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: { case ParamSpec::Type::Choice: {
std::vector<Glib::ustring> choices(spec.choices.begin(), spec.choices.end()); std::vector<Glib::ustring> choices(spec.choices.begin(), spec.choices.end());
auto dropdown = std::make_unique<Gtk::DropDown>(); auto dropdown = std::make_unique<Gtk::DropDown>();
@@ -1051,6 +1066,14 @@ ParamValues EffectorWindow::paramValues() const {
values[entry.spec.key] = formatColor(rgb); values[entry.spec.key] = formatColor(rgb);
break; 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<Gtk::FontDialogButton*>(entry.widget)->get_font_desc().get_family();
if (!family.empty()) values[entry.spec.key] = std::string(family);
break;
}
case ParamSpec::Type::Choice: { case ParamSpec::Type::Choice: {
const auto index = static_cast<Gtk::DropDown*>(entry.widget)->get_selected(); const auto index = static_cast<Gtk::DropDown*>(entry.widget)->get_selected();
if (index < entry.spec.choices.size()) values[entry.spec.key] = entry.spec.choices[index]; if (index < entry.spec.choices.size()) values[entry.spec.key] = entry.spec.choices[index];
+43
View File
@@ -591,6 +591,48 @@ void testVideo(Gtk::Window& window, const std::filesystem::path& dir) {
stack->set_visible_child("chain"); 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<Glib::ustring>::create(Glib::filename_to_uri(path.string())));
pump(300);
auto* chainList = named<Gtk::ListBox>(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<Gtk::FontDialogButton*> buttons;
collect<Gtk::FontDialogButton>(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 // 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. // saving it then must not quietly drop them from the file.
void testMemoryGuard(Gtk::Window& window, const std::filesystem::path& dir) { void testMemoryGuard(Gtk::Window& window, const std::filesystem::path& dir) {
@@ -658,6 +700,7 @@ int main(int argc, char** argv) {
testHelpWindows(*window); testHelpWindows(*window);
testRecentProjects(*window, dataDir / "projects"); testRecentProjects(*window, dataDir / "projects");
testVideo(*window, dataDir / "video"); testVideo(*window, dataDir / "video");
testFont(*window, dataDir / "font");
testMemoryGuard(*window, dataDir / "memory"); testMemoryGuard(*window, dataDir / "memory");
app->remove_window(*window); app->remove_window(*window);
}); });