Add an image overlay effect

Lays a single picture from disk over its input: fitted, filled, stretched,
left at its own size or tiled, centred or pinned to the top left corner,
nudged by an offset and mixed in by its opacity.

The file it needs is a new kind of param, Path, so the interface shows a
chooser and the CLI takes a file name, both without knowing an overlay
exists. Projects write it relative to their own folder, as they do a
source, and Image::load can now hand back the alpha channel instead of
flattening it onto white, so a cut-out PNG keeps its cut-out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-18 19:31:09 +02:00
co-authored by Claude Opus 5
parent 2a64ab115e
commit e2252ecd30
15 changed files with 520 additions and 8 deletions
+59 -1
View File
@@ -293,6 +293,28 @@ void EffectorWindow::buildParamWidgets() {
widget = std::move(spin);
break;
}
case ParamSpec::Type::Path: {
// An entry holding the path, with a chooser beside it; the
// entry is what paramValues() reads back.
auto box = std::make_unique<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
auto* entry = Gtk::make_managed<Gtk::Entry>();
entry->set_hexpand(true);
entry->set_text(paramText(stage.values, spec.key, ""));
entry->set_placeholder_text("No image chosen");
entry->signal_activate().connect(
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
auto* choose = Gtk::make_managed<Gtk::Button>("Choose…");
const int index = selectedStage_;
const std::string key = spec.key;
const std::string title = spec.label;
choose->signal_clicked().connect(
[this, index, key, title] { onChoosePath(index, key, title); });
box->append(*entry);
box->append(*choose);
paramWidgets_.push_back({spec, entry});
widget = std::move(box);
break;
}
case ParamSpec::Type::Choice: {
std::vector<Glib::ustring> choices(spec.choices.begin(), spec.choices.end());
auto dropdown = std::make_unique<Gtk::DropDown>();
@@ -311,7 +333,9 @@ void EffectorWindow::buildParamWidgets() {
widget->set_hexpand(true);
paramGrid_.attach(*label, 0, row);
paramGrid_.attach(*widget, 1, row);
paramWidgets_.push_back({spec, widget.get()});
// A path attaches a box but reads back from the entry inside it, which
// it has registered itself.
if (spec.type != ParamSpec::Type::Path) paramWidgets_.push_back({spec, widget.get()});
ownedWidgets_.push_back(std::move(label));
ownedWidgets_.push_back(std::move(widget));
++row;
@@ -333,6 +357,10 @@ ParamValues EffectorWindow::paramValues() const {
case ParamSpec::Type::Double:
values[entry.spec.key] = static_cast<Gtk::SpinButton*>(entry.widget)->get_value();
break;
case ParamSpec::Type::Path:
values[entry.spec.key] =
std::string(static_cast<Gtk::Entry*>(entry.widget)->get_text());
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];
@@ -809,6 +837,36 @@ void EffectorWindow::onParamChanged() {
requestRender();
}
void EffectorWindow::onChoosePath(int stage, const std::string& key, const std::string& label) {
auto dialog = Gtk::FileDialog::create();
dialog->set_title("Select the " + label);
auto filter = Gtk::FileFilter::create();
filter->set_name("Images");
for (const char* pattern : kImagePatterns) filter->add_pattern(pattern);
auto filters = Gio::ListStore<Gtk::FileFilter>::create();
filters->append(filter);
dialog->set_filters(filters);
dialog->set_default_filter(filter);
dialog->open(*this, [this, dialog, stage, key](const Glib::RefPtr<Gio::AsyncResult>& result) {
std::string path;
try {
const auto file = dialog->open_finish(result);
if (file) path = file->get_path();
} catch (const Glib::Error&) {
return; // cancelled
}
if (path.empty() || stage < 0 || static_cast<std::size_t>(stage) >= stages_.size()) return;
// The widgets may be showing another stage by now: store what they hold
// first, then write the path into the stage that asked for it.
storeParams();
stages_[static_cast<std::size_t>(stage)].values[key] = path;
if (stage == selectedStage_) buildParamWidgets();
requestRender();
});
}
void EffectorWindow::onSeedEntered() {
try {
seed_ = std::stoull(seedEntry_.get_text());