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
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
+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`
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<string, variant<long,double,bool,string>>`), read back with
`paramInt`/`paramDouble`/`paramBool`/`paramChoice`.
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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')
+1
View File
@@ -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: {
+12
View File
@@ -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;
+6 -1
View File
@@ -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<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;
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<ParamSpec>& 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: {
+1 -1
View File
@@ -16,7 +16,7 @@ const std::vector<ParamSpec>& TextEffect::params() const {
static const std::vector<ParamSpec> 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(),
+23
View File
@@ -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::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: {
std::vector<Glib::ustring> choices(spec.choices.begin(), spec.choices.end());
auto dropdown = std::make_unique<Gtk::DropDown>();
@@ -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<Gtk::FontDialogButton*>(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<Gtk::DropDown*>(entry.widget)->get_selected();
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");
}
// 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
// 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);
});