Files
effector/src/gui/window.hpp
T
SiandClaude Opus 5 d48a81c665 Offer standard web and print canvas formats
The canvas was two spin buttons, so "an Instagram story" or "A4 at 300 dpi"
was a number the user had to work out. src/core/canvas.cpp now holds one
table of standard sizes both front-ends read: a web format is a fixed pixel
size, a print format a sheet in millimetres that becomes pixels at a dpi.
Adding a format is a line in canvasFormats() — the dropdown, --format and
--help all walk the table.

The pixels stay the truth everywhere; renderChain never hears about formats.
A format is the presentation of a canvas, carried alongside the pixels in
Options and Project and empty for a custom size, and where one is named it
decides the pixels — so a project saved as A4 reopens as A4 rather than as
whatever it happened to measure.

In the interface a format fills the width and height in and greys them out
rather than hiding them: the size a sheet works out to is worth seeing. A
print format also shows a DPI row, and Custom hands the spins back keeping
the last size, which makes starting from a format and then tweaking it the
ordinary path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:34:17 +02:00

188 lines
7.0 KiB
C++

#pragma once
#include <gtkmm.h>
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>
#include "cli/options.hpp"
#include "core/chain.hpp"
#include "core/effect.hpp"
#include "core/image.hpp"
#include "core/project.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 storeParams(); // widgets -> the selected stage's values
void applyInitial(const Options& initial);
bool canRender() const; // a chain, and sources if the chain needs them
std::string missingText() const; // what is still missing before a render
void showPlaceholder(); // drops the preview and says what is missing
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); // decodes in the background
void onLoadProgress(); // dispatcher: one file finished decoding
void onLoadFinished(); // dispatcher: the batch is done or was cancelled
void showLoadWindow();
void closeLoadWindow();
void cancelLoad();
void appendSourceRow(const std::string& path);
void updateSourcesLabel(); // the collapsed expander shows the count
Gtk::Widget& buildCanvasSection();
// "" while "Custom" is picked, otherwise the format's key.
std::string canvasFormatKey() const;
void setCanvasFormat(const std::string& key, int dpi); // dropdown and dpi
// A format owns the pixel sizes, so the spins follow it and are greyed
// out; "Custom" hands them back to the user untouched.
void syncCanvasWidgets();
void removeSource(Gtk::ListBoxRow* row);
Gtk::Widget& buildChainSection();
Gtk::Widget& buildAddMenu(); // the menu of effects that can be appended
void appendStage(const EffectPtr& effect);
void removeStage(int index);
void moveStage(int index, int delta);
void rebuildChainRows();
void selectStage(int index);
void onStageSelected();
Gtk::Widget& buildMainMenu(); // the header menu: new, open, save project
Project currentProject() const;
void applyProject(const Project& project, const std::string& path);
void clearSources();
void onNewProject();
void onOpenProject();
void onProjectChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void onSaveProject(); // falls back to Save As while the project is untitled
void onSaveProjectAs();
void onSaveProjectChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void writeProject(const std::string& path);
void updateTitle();
void onParamChanged();
// The chooser behind a Path param. The chosen file goes straight into the
// stage's values rather than into the widget, which may well have been
// rebuilt by the time the dialog comes back.
void onChoosePath(int stage, const std::string& key, const std::string& label);
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_;
// The cascade: stage 0 works on the sources, every later stage on the
// output of the one before it.
std::vector<ChainStage> stages_;
int selectedStage_ = 0;
bool suspendStageSelect_ = false; // guards the rebuild of the chain rows
Image result_;
std::uint64_t seed_ = 0;
std::string projectPath_; // empty until the project has been saved once
bool suspendRender_ = true;
// Source loading. Decoding a batch of large photographs takes seconds, so
// it runs off the main thread and reports into a small progress window.
std::thread loader_;
Glib::Dispatcher loadProgress_;
Glib::Dispatcher loadFinished_;
std::atomic<bool> loadCancel_{false};
std::atomic<std::size_t> loadDone_{0};
std::size_t loadTotal_ = 0;
std::mutex loadMutex_;
std::string loadCurrent_;
std::vector<Image> loadedImages_;
std::vector<LoadFailure> loadFailures_;
std::vector<std::string> loadQueue_; // chosen while another batch was loading
bool loading_ = false;
bool dropLoadedImages_ = false; // the batch in flight belongs to a replaced project
sigc::connection loadWindowDelay_;
// 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::Button* addButton_ = nullptr;
// Built fresh with the progress window, and null whenever it is closed.
std::unique_ptr<Gtk::Window> loadWindow_;
Gtk::ProgressBar* loadBar_ = nullptr;
Gtk::Label* loadFileLabel_ = nullptr;
Gtk::Expander sourcesExpander_;
Gtk::ListBox sourceList_;
Gtk::DropDown formatDrop_;
Gtk::SpinButton widthSpin_;
Gtk::SpinButton heightSpin_;
Gtk::SpinButton dpiSpin_;
Gtk::Label* dpiLabel_ = nullptr; // hidden along with the dpi spin
// Set while a format fills the spins, so their own handler does not queue
// a second render on top of the one the format change asks for.
bool suspendCanvasSync_ = false;
Gtk::Grid paramGrid_;
Gtk::Label paramHeading_;
Gtk::Entry seedEntry_;
Gtk::ListBox chainList_;
Gtk::MenuButton addEffectButton_;
Gtk::MenuButton menuButton_;
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