From f61161aef0366695baa66adb6f010296d0b44f8c Mon Sep 17 00:00:00 2001 From: Si Date: Sat, 19 Sep 2026 20:59:56 +0200 Subject: [PATCH] Open on an empty chain, and show the sources only when they are used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The window used to hand the user a Scanlines stage before they had asked for anything, which also meant the first thing on screen was "Add source images to start." — an instruction about an effect nobody chose. It now opens on "No effects yet" and waits: the chain is the subject of the interface, so the first stage is the user's to pick. The default effect was injected by the option parser, which would have put Scanlines straight back on every launch. It moves to renderHeadless, where a chain really is mandatory — a headless run must render something, while the interface has no such need. `-o out.png` with no -e is unchanged. The sources section follows the same question the render gate already asks, chainNeedsSources(): only the first stage is ever handed the sources, so a chain led by blur or text makes the whole section beside the point and it is hidden rather than left there to be filled in vain. Today that shows it for scanlines and tiles, but no effect is named in the GUI — a new one declaring needsSources() gets this for free. rebuildChainRows() is the one funnel every chain change goes through, so that is where the visibility is set. New in the project menu now starts empty too, matching what the window opens with. Co-Authored-By: Claude Opus 5 --- src/cli/options.cpp | 4 ++-- src/gui/window.cpp | 17 +++++++++++++---- src/gui/window.hpp | 2 ++ src/main.cpp | 9 ++++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/cli/options.cpp b/src/cli/options.cpp index ac8437f..582e2ec 100644 --- a/src/cli/options.cpp +++ b/src/cli/options.cpp @@ -160,7 +160,6 @@ Options parseOptions(int argc, char** argv) { if (const auto* format = canvasFormatByKey(options.format)) { canvasFormatSize(*format, options.dpi, options.width, options.height); } - if (options.effects.empty()) options.effects.push_back(defaultEffectName()); return options; } @@ -169,7 +168,8 @@ std::string usage(const char* programName) { out << "Usage: " << programName << " [options] [image ...]\n\n" << "Image effects app. Runs the GTK interface unless --output is given.\n\n" << "Options:\n" - << " -e, --effect NAME effect to apply, repeat to cascade (default: " << defaultEffectName() << ")\n" + << " -e, --effect NAME effect to apply, repeat to cascade (--output" + " defaults to " << defaultEffectName() << ")\n" << " -o, --output FILE render headlessly to this file and exit\n" << " -s, --size WxH canvas size, each side " << kMinCanvasSide << "-" << kMaxCanvasSide << " (default: 1920x1080)\n" diff --git a/src/gui/window.cpp b/src/gui/window.cpp index 59a53f3..355413d 100644 --- a/src/gui/window.cpp +++ b/src/gui/window.cpp @@ -49,7 +49,8 @@ EffectorWindow::EffectorWindow(const Options& initial) { set_default_size(1180, 760); effects_ = effects(); - stages_.push_back({effects_.front(), effects_.front()->defaults()}); + // The chain starts empty: the interface opens on "No effects yet" and the + // user picks the first stage, rather than being handed one at random. seed_ = Rng::randomSeed(); renderDone_.connect(sigc::mem_fun(*this, &EffectorWindow::onRenderFinished)); @@ -191,6 +192,15 @@ Gtk::Widget& EffectorWindow::buildSidebar() { return *sidebar; } +// Only the first stage is ever handed the sources, so whether the chain wants +// them at all is `chainNeedsSources`. An effect that starts from nothing (blur, +// text) makes the whole sources section beside the point, so it is hidden +// rather than left there to be filled in vain. This is the same declarative +// answer the render gate uses -- no effect is named here. +void EffectorWindow::updateSourcesVisible() { + sourcesExpander_.set_visible(chainNeedsSources(stages_)); +} + // The expander is collapsed by default, so its label carries the count that // used to be plain to see in the list. void EffectorWindow::updateSourcesLabel() { @@ -952,6 +962,7 @@ void EffectorWindow::rebuildChainRows() { selectStage(selectedStage_); suspendStageSelect_ = false; + updateSourcesVisible(); } void EffectorWindow::selectStage(int index) { @@ -1242,9 +1253,7 @@ void EffectorWindow::clearSources() { } void EffectorWindow::onNewProject() { - Project fresh; - fresh.stages.push_back({effects_.front(), effects_.front()->defaults()}); - applyProject(fresh, ""); + applyProject(Project(), ""); // same empty chain the window opens with } void EffectorWindow::onOpenProject() { diff --git a/src/gui/window.hpp b/src/gui/window.hpp index 1deb21e..1a5cee9 100644 --- a/src/gui/window.hpp +++ b/src/gui/window.hpp @@ -49,6 +49,8 @@ private: void cancelLoad(); void appendSourceRow(const std::string& path); void updateSourcesLabel(); // the collapsed expander shows the count + // The whole sources section only shows while the chain asks for sources. + void updateSourcesVisible(); Gtk::Widget& buildCanvasSection(); // "" while "Custom" is picked, otherwise the format's key. diff --git a/src/main.cpp b/src/main.cpp index 79fe47c..fe5aa36 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,7 +18,14 @@ namespace { int renderHeadless(const effector::Options& options) { - const auto stages = effector::buildChain(options); + auto stages = effector::buildChain(options); + // A headless run always renders something, so with no -e the default + // effect stands in. The interface has no such need: it opens on an empty + // chain and waits for the user to add the first stage. + if (stages.empty()) { + const auto effect = effector::effectByName(effector::defaultEffectName()); + stages.push_back({effect, effect->defaults()}); + } std::vector failures;