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>
This commit is contained in:
+122
-22
@@ -7,6 +7,7 @@
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
|
||||
#include "core/canvas.hpp"
|
||||
#include "core/chain.hpp"
|
||||
#include "core/project.hpp"
|
||||
#include "core/registry.hpp"
|
||||
@@ -175,28 +176,7 @@ Gtk::Widget& EffectorWindow::buildSidebar() {
|
||||
sidebar->append(sourcesExpander_);
|
||||
|
||||
sidebar->append(*sectionLabel("Canvas"));
|
||||
auto* grid = Gtk::make_managed<Gtk::Grid>();
|
||||
grid->set_column_spacing(8);
|
||||
grid->set_row_spacing(6);
|
||||
widthSpin_.set_range(kMinCanvasSide, kMaxCanvasSide);
|
||||
widthSpin_.set_increments(1, 100);
|
||||
widthSpin_.set_value(1920);
|
||||
heightSpin_.set_range(kMinCanvasSide, kMaxCanvasSide);
|
||||
heightSpin_.set_increments(1, 100);
|
||||
heightSpin_.set_value(1080);
|
||||
for (auto* spin : {&widthSpin_, &heightSpin_}) {
|
||||
spin->set_hexpand(true);
|
||||
spin->signal_value_changed().connect(sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
||||
}
|
||||
auto* widthLabel = Gtk::make_managed<Gtk::Label>("Width");
|
||||
widthLabel->set_xalign(0.0F);
|
||||
auto* heightLabel = Gtk::make_managed<Gtk::Label>("Height");
|
||||
heightLabel->set_xalign(0.0F);
|
||||
grid->attach(*widthLabel, 0, 0);
|
||||
grid->attach(widthSpin_, 1, 0);
|
||||
grid->attach(*heightLabel, 0, 1);
|
||||
grid->attach(heightSpin_, 1, 1);
|
||||
sidebar->append(*grid);
|
||||
sidebar->append(buildCanvasSection());
|
||||
|
||||
auto* seedBox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
|
||||
auto* seedLabel = Gtk::make_managed<Gtk::Label>("Seed");
|
||||
@@ -220,6 +200,122 @@ void EffectorWindow::updateSourcesLabel() {
|
||||
: "Source images (" + std::to_string(count) + ")");
|
||||
}
|
||||
|
||||
// The canvas is either a named format — a web size, or a sheet of paper at a
|
||||
// resolution — or the user's own pixels. A format owns the two spin buttons
|
||||
// while it is picked, which is why they are shown greyed out rather than
|
||||
// hidden: the size a format works out to is worth seeing.
|
||||
Gtk::Widget& EffectorWindow::buildCanvasSection() {
|
||||
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 6);
|
||||
|
||||
// Index 0 is "Custom"; every later index is canvasFormats()[index - 1].
|
||||
std::vector<Glib::ustring> labels{"Custom"};
|
||||
for (const auto& format : canvasFormats()) {
|
||||
labels.push_back(format.label + " (" + canvasFormatDetail(format) + ")");
|
||||
}
|
||||
formatDrop_.set_model(Gtk::StringList::create(labels));
|
||||
formatDrop_.set_selected(0);
|
||||
formatDrop_.set_tooltip_text("Standard web and print sizes, or your own pixels");
|
||||
formatDrop_.property_selected().signal_changed().connect(
|
||||
sigc::mem_fun(*this, &EffectorWindow::syncCanvasWidgets));
|
||||
box->append(formatDrop_);
|
||||
|
||||
auto* grid = Gtk::make_managed<Gtk::Grid>();
|
||||
grid->set_column_spacing(8);
|
||||
grid->set_row_spacing(6);
|
||||
widthSpin_.set_range(kMinCanvasSide, kMaxCanvasSide);
|
||||
widthSpin_.set_increments(1, 100);
|
||||
widthSpin_.set_value(1920);
|
||||
heightSpin_.set_range(kMinCanvasSide, kMaxCanvasSide);
|
||||
heightSpin_.set_increments(1, 100);
|
||||
heightSpin_.set_value(1080);
|
||||
dpiSpin_.set_range(kMinDpi, kMaxDpi);
|
||||
dpiSpin_.set_increments(1, 50);
|
||||
dpiSpin_.set_value(kDefaultDpi);
|
||||
for (auto* spin : {&widthSpin_, &heightSpin_, &dpiSpin_}) {
|
||||
spin->set_hexpand(true);
|
||||
}
|
||||
for (auto* spin : {&widthSpin_, &heightSpin_}) {
|
||||
spin->signal_value_changed().connect([this] {
|
||||
if (!suspendCanvasSync_) onParamChanged();
|
||||
});
|
||||
}
|
||||
// The dpi decides the pixels of a sheet, so moving it goes through the
|
||||
// same path a format change does.
|
||||
dpiSpin_.signal_value_changed().connect([this] {
|
||||
if (!suspendCanvasSync_) syncCanvasWidgets();
|
||||
});
|
||||
|
||||
auto* widthLabel = Gtk::make_managed<Gtk::Label>("Width");
|
||||
widthLabel->set_xalign(0.0F);
|
||||
auto* heightLabel = Gtk::make_managed<Gtk::Label>("Height");
|
||||
heightLabel->set_xalign(0.0F);
|
||||
auto* dpiLabel = Gtk::make_managed<Gtk::Label>("DPI");
|
||||
dpiLabel->set_xalign(0.0F);
|
||||
dpiLabel_ = dpiLabel;
|
||||
grid->attach(*widthLabel, 0, 0);
|
||||
grid->attach(widthSpin_, 1, 0);
|
||||
grid->attach(*heightLabel, 0, 1);
|
||||
grid->attach(heightSpin_, 1, 1);
|
||||
grid->attach(*dpiLabel, 0, 2);
|
||||
grid->attach(dpiSpin_, 1, 2);
|
||||
box->append(*grid);
|
||||
|
||||
// Quietly: the sidebar is still being built, and nothing is to be rendered
|
||||
// until the initial options or a project have had their say.
|
||||
suspendCanvasSync_ = true;
|
||||
syncCanvasWidgets();
|
||||
suspendCanvasSync_ = false;
|
||||
return *box;
|
||||
}
|
||||
|
||||
std::string EffectorWindow::canvasFormatKey() const {
|
||||
const auto selected = formatDrop_.get_selected();
|
||||
if (selected == GTK_INVALID_LIST_POSITION || selected == 0) return {};
|
||||
const auto index = static_cast<std::size_t>(selected) - 1;
|
||||
const auto& formats = canvasFormats();
|
||||
return index < formats.size() ? formats[index].key : std::string();
|
||||
}
|
||||
|
||||
// A key the build does not know — an old project, or a hand-edited file — lands
|
||||
// on "Custom", which leaves the pixels the project carries alone.
|
||||
void EffectorWindow::setCanvasFormat(const std::string& key, int dpi) {
|
||||
const bool suspended = suspendCanvasSync_;
|
||||
suspendCanvasSync_ = true;
|
||||
dpiSpin_.set_value(std::clamp(dpi, kMinDpi, kMaxDpi));
|
||||
guint selected = 0;
|
||||
const auto& formats = canvasFormats();
|
||||
for (std::size_t i = 0; i < formats.size(); ++i) {
|
||||
if (formats[i].key == key) selected = static_cast<guint>(i + 1);
|
||||
}
|
||||
formatDrop_.set_selected(selected);
|
||||
syncCanvasWidgets(); // still suspended, so the spins follow without a render
|
||||
suspendCanvasSync_ = suspended;
|
||||
}
|
||||
|
||||
void EffectorWindow::syncCanvasWidgets() {
|
||||
// Suspended means a caller is putting the canvas together — the initial
|
||||
// options, or a project being opened — and will ask for the render itself.
|
||||
const bool quiet = suspendCanvasSync_;
|
||||
const auto* format = canvasFormatByKey(canvasFormatKey());
|
||||
const bool print = format != nullptr && format->print;
|
||||
dpiLabel_->set_visible(print);
|
||||
dpiSpin_.set_visible(print);
|
||||
widthSpin_.set_sensitive(format == nullptr);
|
||||
heightSpin_.set_sensitive(format == nullptr);
|
||||
|
||||
if (format != nullptr) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
canvasFormatSize(*format, static_cast<int>(dpiSpin_.get_value()), width, height);
|
||||
const bool suspended = suspendCanvasSync_;
|
||||
suspendCanvasSync_ = true; // one render for the change, not one per spin
|
||||
widthSpin_.set_value(width);
|
||||
heightSpin_.set_value(height);
|
||||
suspendCanvasSync_ = suspended;
|
||||
}
|
||||
if (!quiet) onParamChanged();
|
||||
}
|
||||
|
||||
Gtk::Widget& EffectorWindow::buildPreview() {
|
||||
picture_.set_can_shrink(true);
|
||||
picture_.set_content_fit(Gtk::ContentFit::CONTAIN);
|
||||
@@ -429,6 +525,7 @@ void EffectorWindow::applyInitial(const Options& initial) {
|
||||
|
||||
widthSpin_.set_value(initial.width);
|
||||
heightSpin_.set_value(initial.height);
|
||||
setCanvasFormat(initial.format, initial.dpi); // a format fills the spins itself
|
||||
if (initial.seed) {
|
||||
seed_ = *initial.seed;
|
||||
seedEntry_.set_text(std::to_string(seed_));
|
||||
@@ -1098,6 +1195,8 @@ Project EffectorWindow::currentProject() const {
|
||||
project.stages = stages_;
|
||||
project.width = static_cast<int>(widthSpin_.get_value());
|
||||
project.height = static_cast<int>(heightSpin_.get_value());
|
||||
project.format = canvasFormatKey();
|
||||
project.dpi = static_cast<int>(dpiSpin_.get_value());
|
||||
project.seed = seed_;
|
||||
return project;
|
||||
}
|
||||
@@ -1114,6 +1213,7 @@ void EffectorWindow::applyProject(const Project& project, const std::string& pat
|
||||
|
||||
widthSpin_.set_value(project.width);
|
||||
heightSpin_.set_value(project.height);
|
||||
setCanvasFormat(project.format, project.dpi);
|
||||
seed_ = project.seed.value_or(Rng::randomSeed());
|
||||
seedEntry_.set_text(std::to_string(seed_));
|
||||
|
||||
|
||||
@@ -49,6 +49,14 @@ private:
|
||||
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();
|
||||
@@ -146,8 +154,14 @@ private:
|
||||
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_;
|
||||
|
||||
Reference in New Issue
Block a user