Let effects declare whether they need source images
Whether an effect works from the loaded photographs is now part of the declarative plug-in contract, next to params(): Effect::needsSources() defaults to true, and blur — which only softens what it is handed — says no and blurs an empty canvas when given nothing. Only the first stage of a cascade ever sees the sources, so chainNeedsSources() just asks the first stage. Both front-ends gate on that instead of on "are there any images": the CLI only refuses an empty batch when the chain wants one, and the interface renders a blur-led chain with no images loaded, saying whichever thing is actually missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+43
-27
@@ -57,7 +57,13 @@ EffectorWindow::EffectorWindow(const Options& initial) {
|
||||
applyInitial(initial);
|
||||
|
||||
suspendRender_ = false;
|
||||
if (!sources_->empty()) startRender();
|
||||
if (loading_) {
|
||||
placeholder_.set_text(missingText());
|
||||
} else if (canRender()) {
|
||||
startRender();
|
||||
} else {
|
||||
showPlaceholder();
|
||||
}
|
||||
}
|
||||
|
||||
EffectorWindow::~EffectorWindow() {
|
||||
@@ -569,15 +575,7 @@ void EffectorWindow::removeSource(Gtk::ListBoxRow* row) {
|
||||
}
|
||||
});
|
||||
sourceList_.remove(*row);
|
||||
|
||||
if (!sources_->empty()) {
|
||||
requestRender();
|
||||
} else {
|
||||
result_ = Image();
|
||||
saveButton_.set_sensitive(false);
|
||||
previewStack_.set_visible_child("placeholder");
|
||||
status_.set_text("");
|
||||
}
|
||||
requestRender();
|
||||
}
|
||||
|
||||
void EffectorWindow::onAddImages() {
|
||||
@@ -680,14 +678,6 @@ void EffectorWindow::removeStage(int index) {
|
||||
rebuildChainRows();
|
||||
buildParamWidgets();
|
||||
|
||||
if (stages_.empty()) {
|
||||
result_ = Image();
|
||||
saveButton_.set_sensitive(false);
|
||||
previewStack_.set_visible_child("placeholder");
|
||||
placeholder_.set_text("Add an effect to start.");
|
||||
status_.set_text("");
|
||||
return;
|
||||
}
|
||||
requestRender();
|
||||
}
|
||||
|
||||
@@ -794,15 +784,40 @@ void EffectorWindow::onSeedEntered() {
|
||||
void EffectorWindow::onRecreate() {
|
||||
seed_ = Rng::randomSeed();
|
||||
seedEntry_.set_text(std::to_string(seed_));
|
||||
if (sources_->empty()) {
|
||||
showError("Add at least one source image first.");
|
||||
if (!canRender()) {
|
||||
showError(missingText());
|
||||
return;
|
||||
}
|
||||
requestRender();
|
||||
}
|
||||
|
||||
// Whether the effect chain has everything it needs. Only the first stage is
|
||||
// ever handed the sources, so an effect that does not want them (blur) renders
|
||||
// happily with none loaded.
|
||||
bool EffectorWindow::canRender() const {
|
||||
if (stages_.empty()) return false;
|
||||
return !sources_->empty() || !chainNeedsSources(stages_);
|
||||
}
|
||||
|
||||
std::string EffectorWindow::missingText() const {
|
||||
if (stages_.empty()) return "Add an effect to start.";
|
||||
return "Add source images to start.";
|
||||
}
|
||||
|
||||
void EffectorWindow::showPlaceholder() {
|
||||
result_ = Image();
|
||||
saveButton_.set_sensitive(false);
|
||||
placeholder_.set_text(missingText());
|
||||
previewStack_.set_visible_child("placeholder");
|
||||
status_.set_text("");
|
||||
}
|
||||
|
||||
void EffectorWindow::requestRender() {
|
||||
if (suspendRender_ || sources_->empty() || stages_.empty()) return;
|
||||
if (suspendRender_) return;
|
||||
if (!canRender()) {
|
||||
showPlaceholder();
|
||||
return;
|
||||
}
|
||||
// Spin buttons fire on every step while they are held down; coalesce those
|
||||
// into one render instead of queueing a job per tick.
|
||||
debounce_.disconnect();
|
||||
@@ -815,7 +830,7 @@ void EffectorWindow::requestRender() {
|
||||
}
|
||||
|
||||
void EffectorWindow::startRender() {
|
||||
if (sources_->empty() || stages_.empty()) return;
|
||||
if (!canRender()) return;
|
||||
if (rendering_) {
|
||||
staleResult_ = true; // a fresh render follows this one
|
||||
return;
|
||||
@@ -893,9 +908,11 @@ void EffectorWindow::onRenderFinished() {
|
||||
std::ostringstream status;
|
||||
status.setf(std::ios::fixed);
|
||||
status.precision(1);
|
||||
status << chainTitle(stages_) << " · " << result_.width() << "×" << result_.height() << " · "
|
||||
<< sources_->size() << (sources_->size() == 1 ? " source" : " sources") << " · seed "
|
||||
<< seed_ << " · " << millis << " ms";
|
||||
status << chainTitle(stages_) << " · " << result_.width() << "×" << result_.height() << " · ";
|
||||
if (!sources_->empty()) {
|
||||
status << sources_->size() << (sources_->size() == 1 ? " source" : " sources") << " · ";
|
||||
}
|
||||
status << "seed " << seed_ << " · " << millis << " ms";
|
||||
status_.set_text(status.str());
|
||||
}
|
||||
|
||||
@@ -970,8 +987,7 @@ void EffectorWindow::applyProject(const Project& project, const std::string& pat
|
||||
suspendRender_ = false;
|
||||
|
||||
if (project.sources.empty()) {
|
||||
placeholder_.set_text("Add source images to start.");
|
||||
status_.set_text("");
|
||||
requestRender(); // a chain that needs no sources still has a picture
|
||||
} else {
|
||||
addPaths(project.sources); // renders once the images are decoded
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user