Effector: C++/CMake image effects app

Port of the Python prototype to C++20 with a CMake build:

- effector_core: GUI-free static library (effects, image IO via gdk-pixbuf,
  declarative param specs, registry, portable seeded RNG, thread helper)
- scanlines effect: plans strips from the seeded RNG, then copies pixels in
  parallel with memcpy and doubling horizontal tiling
- gtkmm 4 interface: native file dialogs, widgets generated from the param
  specs, coalesced background renders, preview as a GDK memory texture
- CLI: flags generated from the effect registry, headless rendering with no
  display required
- ctest suite covering the RNG, registry, effect geometry and image round trip

The Python prototype is kept under python-reference/.
This commit is contained in:
hans
2026-09-18 14:28:13 +02:00
commit 925a719da0
36 changed files with 2859 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include <gtkmm.h>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>
#include "cli/options.hpp"
#include "core/effect.hpp"
#include "core/image.hpp"
namespace effector {
class EffectorWindow : public Gtk::ApplicationWindow {
public:
explicit EffectorWindow(const Options& initial);
~EffectorWindow() override;
private:
void buildUi();
Gtk::Widget& buildSidebar();
Gtk::Widget& buildPreview();
void buildParamWidgets();
ParamValues paramValues() const;
void applyInitial(const Options& initial);
void requestRender(); // coalesced: restarts the short debounce timer
void startRender(); // starts a worker, or marks the result stale
void onAddImages();
void onImagesChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void addPaths(const std::vector<std::string>& paths);
void appendSourceRow(const std::string& path);
void removeSource(Gtk::ListBoxRow* row);
void onEffectChanged();
void onParamChanged();
void onSeedEntered();
void onRecreate();
void onSave();
void onSaveChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void onRenderFinished(); // runs on the main thread via the dispatcher
// Sources are handed to the worker as an immutable snapshot, so adding or
// removing an image while a render is in flight cannot race with it.
using SourceList = std::vector<Image>;
template <typename Mutate>
void mutateSources(Mutate&& mutate);
void showError(const std::string& message);
void clearError();
// Model
std::shared_ptr<const SourceList> sources_ = std::make_shared<SourceList>();
std::vector<EffectPtr> effects_;
EffectPtr effect_;
Image result_;
std::uint64_t seed_ = 0;
bool suspendRender_ = true;
// Render job
Glib::Dispatcher renderDone_;
std::thread worker_;
std::mutex resultMutex_;
bool rendering_ = false;
bool staleResult_ = false;
sigc::connection debounce_;
Image pendingImage_;
std::string pendingError_;
double pendingMillis_ = 0.0;
// Widgets
Gtk::Label banner_;
Gtk::ListBox sourceList_;
Gtk::SpinButton widthSpin_;
Gtk::SpinButton heightSpin_;
Gtk::Grid paramGrid_;
Gtk::Entry seedEntry_;
Gtk::DropDown effectDropdown_;
Gtk::Button recreateButton_{"Recreate"};
Gtk::Button saveButton_{"Save As…"};
Gtk::Picture picture_;
Gtk::Stack previewStack_;
Gtk::Label placeholder_;
Gtk::Spinner spinner_;
Gtk::Label status_;
struct ParamWidget {
ParamSpec spec;
Gtk::Widget* widget = nullptr;
};
std::vector<ParamWidget> paramWidgets_;
std::vector<std::unique_ptr<Gtk::Widget>> ownedWidgets_;
};
} // namespace effector