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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user