diff --git a/Meta/include/meta/metadata/keys.hpp b/Meta/include/meta/metadata/keys.hpp index 64f16ba..e8b8cbe 100644 --- a/Meta/include/meta/metadata/keys.hpp +++ b/Meta/include/meta/metadata/keys.hpp @@ -31,6 +31,18 @@ namespace meta::keys::ui inline constexpr char category[] = "ui.category"; inline constexpr char closed[] = "ui.closed"; inline constexpr char data_provider[] = "ui.data_provider"; + +/** @brief Highest value the rail spans, when that is lower than the real max. + * + * A rail has to end somewhere comfortable to drag across, but the parameter + * behind it may legitimately accept far more. Spatial Frequency is the case + * that prompted this: 0 to 64 is the useful range to aim at, yet a user who + * knows they want 512 should be able to type it. + * + * Absent, the rail spans to constraints::max and typing is bounded by the same + * value, which is the behaviour every attribute had before this key existed. + */ +inline constexpr char drag_max[] = "ui.drag_max"; inline constexpr char format[] = "ui.format"; inline constexpr char label[] = "ui.label"; inline constexpr char label_false[] = "ui.label_false"; diff --git a/MetaUI/qt/include/meta_qt/designs/industrial/editor_style.hpp b/MetaUI/qt/include/meta_qt/designs/industrial/editor_style.hpp index 33600f0..0d0b86e 100644 --- a/MetaUI/qt/include/meta_qt/designs/industrial/editor_style.hpp +++ b/MetaUI/qt/include/meta_qt/designs/industrial/editor_style.hpp @@ -33,35 +33,43 @@ inline void style_editor(QWidget *host, const Theme &theme) "#IndustrialEditor QLabel { color: %1; background: transparent; }" "#IndustrialEditor QPushButton, #IndustrialEditor QToolButton, " "#IndustrialEditor QComboBox {" - " color: %1; background: %2; border: 1px solid %3; border-radius: 5px; " + " color: %1; background: %2; border: 1px solid %3; border-radius: " + "5px; " "padding: 3px 7px; }" - "#IndustrialEditor QPushButton:hover, #IndustrialEditor QToolButton:hover { " + "#IndustrialEditor QPushButton:hover, #IndustrialEditor " + "QToolButton:hover { " "border-color: %4; }" "#IndustrialEditor QPushButton:checked { background: %4; color: %1; }" "#IndustrialEditor QPushButton:disabled, #IndustrialEditor " - "QToolButton:disabled { color: %5; background: transparent; border-color: %3; }" - "#IndustrialEditor QScrollArea { border: none; background: transparent; }" - "#IndustrialEditor QScrollBar:vertical { width: 8px; background: transparent; }" + "QToolButton:disabled { color: %5; background: transparent; " + "border-color: %3; }" + "#IndustrialEditor QScrollArea { border: none; background: " + "transparent; }" + "#IndustrialEditor QScrollBar:vertical { width: 8px; background: " + "transparent; }" "#IndustrialEditor QScrollBar::handle:vertical { background: %3; " "border-radius: 3px; min-height: 24px; }" "#IndustrialEditor QScrollBar::add-line:vertical, #IndustrialEditor " "QScrollBar::sub-line:vertical { height: 0px; }" "#IndustrialEditor QScrollBar::add-page:vertical, #IndustrialEditor " "QScrollBar::sub-page:vertical { background: transparent; }" - "#IndustrialEditor QComboBox::drop-down { border: none; width: 20px; }" + "#IndustrialEditor QComboBox::drop-down { border: none; width: 20px; " + "}" "#IndustrialEditor QToolButton::menu-indicator { image: none; }" - "#IndustrialEditor QPushButton[preset_name] { padding: 2px; border: 2px solid " + "#IndustrialEditor QPushButton[preset_name] { padding: 2px; border: " + "2px solid " "transparent; border-radius: 5px; background: transparent; }" - "#IndustrialEditor QPushButton[preset_name]:hover { border-color: %5; }" - "#IndustrialEditor QPushButton[preset_name]:checked { border-color: %4; " + "#IndustrialEditor QPushButton[preset_name]:hover { border-color: " + "%5; }" + "#IndustrialEditor QPushButton[preset_name]:checked { border-color: " + "%4; " "background: transparent; }") .arg(theme.ink_primary.name(), theme.field.name(), theme.field_border.name(), theme.accent.name(), theme.ink_dim.name())); - if (host->layout()) - host->layout()->setSpacing(8); + if (host->layout()) host->layout()->setSpacing(8); for (auto *child : host->findChildren()) { child->setProperty("industrialEditor", true); @@ -79,8 +87,7 @@ inline void style_editor(QWidget *host, const Theme &theme) } for (auto *button : host->findChildren()) { - if (button->property("preset_name").isValid()) - continue; + if (button->property("preset_name").isValid()) continue; button->setFont(ui_font(12)); button->setFixedHeight(28); button->setCursor(Qt::PointingHandCursor); diff --git a/MetaUI/qt/include/meta_qt/designs/industrial/linked_sliders.hpp b/MetaUI/qt/include/meta_qt/designs/industrial/linked_sliders.hpp index 6ff7511..d0009e7 100644 --- a/MetaUI/qt/include/meta_qt/designs/industrial/linked_sliders.hpp +++ b/MetaUI/qt/include/meta_qt/designs/industrial/linked_sliders.hpp @@ -15,7 +15,9 @@ class LinkedSliders : public Control { Q_OBJECT public: - LinkedSliders(Attribute &, const RowContext &, QWidget * = nullptr); + LinkedSliders(Attribute &, + const RowContext &, + QWidget * = nullptr); static bool can_render(const Attribute &) { return true; } glm::vec2 get() const override { return value_; } void set(const glm::vec2 &) override; diff --git a/MetaUI/qt/include/meta_qt/designs/industrial/text_row.hpp b/MetaUI/qt/include/meta_qt/designs/industrial/text_row.hpp index beba103..2ab4530 100644 --- a/MetaUI/qt/include/meta_qt/designs/industrial/text_row.hpp +++ b/MetaUI/qt/include/meta_qt/designs/industrial/text_row.hpp @@ -32,7 +32,9 @@ class TextRow : public Control Q_OBJECT public: - TextRow(Attribute &attr, const RowContext &ctx, QWidget *parent = nullptr); + TextRow(Attribute &attr, + const RowContext &ctx, + QWidget *parent = nullptr); /// Any string attribute. There is no metadata this row cannot honour. static bool can_render(const Attribute &attr); diff --git a/MetaUI/qt/include/meta_qt/ui/number_format.hpp b/MetaUI/qt/include/meta_qt/ui/number_format.hpp index 863cc0d..c99ac41 100644 --- a/MetaUI/qt/include/meta_qt/ui/number_format.hpp +++ b/MetaUI/qt/include/meta_qt/ui/number_format.hpp @@ -11,14 +11,13 @@ namespace meta::qt inline QString display_float(float value, int decimals = 2) { const double magnitude = std::abs(double(value)); - if (!std::isfinite(value)) - return QString::number(value); + if (!std::isfinite(value)) return QString::number(value); decimals = std::clamp(decimals, 0, 8); if (magnitude == 0 || magnitude >= std::pow(10., -decimals)) return QString::number(value == 0 ? 0 : value, 'f', decimals); - if (magnitude < 1e-10) - return QString::number(value, 'g', 6); - const int precision = std::min(12, int(std::ceil(-std::log10(magnitude))) + 5); + if (magnitude < 1e-10) return QString::number(value, 'g', 6); + const int precision = std::min(12, + int(std::ceil(-std::log10(magnitude))) + 5); QString text = QString::number(value, 'f', precision); while (text.endsWith('0') && text.size() - text.indexOf('.') - 1 > decimals) text.chop(1); diff --git a/MetaUI/qt/include/meta_qt/widgets/array_canvas.hpp b/MetaUI/qt/include/meta_qt/widgets/array_canvas.hpp index 99e578c..9563cf4 100644 --- a/MetaUI/qt/include/meta_qt/widgets/array_canvas.hpp +++ b/MetaUI/qt/include/meta_qt/widgets/array_canvas.hpp @@ -22,7 +22,7 @@ class ArrayCanvas : public QWidget QSize sizeHint() const override; QSize minimumSizeHint() const override { return QSize(120, 120); } - int heightForWidth(int width) const override { return width; } + int heightForWidth(int width) const override { return width; } void set_field_data(const std::vector &data); const std::vector &get_field_data() const; diff --git a/MetaUI/qt/include/meta_qt/widgets/points_canvas.hpp b/MetaUI/qt/include/meta_qt/widgets/points_canvas.hpp index 8917b3d..5bb7cdb 100644 --- a/MetaUI/qt/include/meta_qt/widgets/points_canvas.hpp +++ b/MetaUI/qt/include/meta_qt/widgets/points_canvas.hpp @@ -53,17 +53,17 @@ class PointsCanvas : public QWidget bool closed = false, QWidget *parent = nullptr); - void clear_all(); + void clear_all(); QSize sizeHint() const override { return QSize(320, 320); } QSize minimumSizeHint() const override { return QSize(120, 120); } - int heightForWidth(int width) const override { return width; } - void randomize(int count); - void load_csv(const QString &path); // x,y,z per line (z clamped to [0,1]) - void set_points(const std::vector &new_points); - void set_background_image(const std::vector &pixels, - int w, - int h, - int channels); + int heightForWidth(int width) const override { return width; } + void randomize(int count); + void load_csv(const QString &path); // x,y,z per line (z clamped to [0,1]) + void set_points(const std::vector &new_points); + void set_background_image(const std::vector &pixels, + int w, + int h, + int channels); Q_SIGNALS: void points_changed(); @@ -93,11 +93,11 @@ class PointsCanvas : public QWidget std::vector &points_; float min_x_, max_x_, min_y_, max_y_, z_step_; - int hovered_idx_ = -1; + int hovered_idx_ = -1; QString order_input_; - int drag_idx_ = -1; - bool moved_during_drag_ = false; - int hovered_segment_ = -1; + int drag_idx_ = -1; + bool moved_during_drag_ = false; + int hovered_segment_ = -1; Mode mode_; bool closed_; diff --git a/MetaUI/qt/include/meta_qt/widgets/range_bar.hpp b/MetaUI/qt/include/meta_qt/widgets/range_bar.hpp index 30a15ba..e8597be 100644 --- a/MetaUI/qt/include/meta_qt/widgets/range_bar.hpp +++ b/MetaUI/qt/include/meta_qt/widgets/range_bar.hpp @@ -5,9 +5,9 @@ #include +#include "meta_qt/ui/theme.hpp" #include #include -#include "meta_qt/ui/theme.hpp" namespace meta::qt { @@ -34,7 +34,12 @@ class RangeBar : public QWidget QWidget *parent = nullptr); void set_value(glm::vec2 v); - void set_theme(const Theme &theme) { theme_ = theme; industrial_ = true; update(); } + void set_theme(const Theme &theme) + { + theme_ = theme; + industrial_ = true; + update(); + } void set_histogram(const std::vector &x, const std::vector &y); Q_SIGNALS: @@ -50,7 +55,7 @@ class RangeBar : public QWidget private: Theme theme_; - bool industrial_ = false; + bool industrial_ = false; enum class Handle { None, diff --git a/MetaUI/qt/src/designs/industrial/industrial.cpp b/MetaUI/qt/src/designs/industrial/industrial.cpp index 7e76d8f..3af172d 100644 --- a/MetaUI/qt/src/designs/industrial/industrial.cpp +++ b/MetaUI/qt/src/designs/industrial/industrial.cpp @@ -90,39 +90,63 @@ void register_design() // construction yields a complete panel rather than a handful of rows. Drop // this line and the unported widget types simply render nothing. stock::register_design(); - const RowFactory editor = [](AbstractAttribute &attr, const RowContext &ctx, - QWidget *parent) -> MetaWidget * + const RowFactory editor = [](AbstractAttribute &attr, + const RowContext &ctx, + QWidget *parent) -> MetaWidget * { - auto *widget = DesignRegistry::instance().render(&attr, stock::kDesignName, ctx, parent); + auto *widget = DesignRegistry::instance().render(&attr, + stock::kDesignName, + ctx, + parent); if (!widget) return nullptr; #ifdef META_ENABLE_ARRAY_TYPES if (auto *canvas = widget->findChild()) { auto *layout = qobject_cast(widget->layout()); - layout->insertWidget(0, new QLabel(QString::fromStdString(meta::common::label(static_cast &>(attr))), widget)); - auto *hint = new QLabel(QObject::tr("Drag to paint · Right-drag to erase · Scroll to resize"), widget); + layout->insertWidget( + 0, + new QLabel(QString::fromStdString(meta::common::label( + static_cast &>(attr))), + widget)); + auto *hint = new QLabel( + QObject::tr("Drag to paint · Right-drag to erase · Scroll to resize"), + widget); hint->setWordWrap(true); layout->addWidget(hint); canvas->setProperty("industrialEditor", true); } #endif - style_editor(widget, ctx.theme ? *ctx.theme : DesignRegistry::instance().theme(kDesignName)); - for (auto *range : widget->findChildren()) { - range->set_theme(ctx.theme ? *ctx.theme : DesignRegistry::instance().theme(kDesignName)); + style_editor(widget, + ctx.theme ? *ctx.theme + : DesignRegistry::instance().theme(kDesignName)); + for (auto *range : widget->findChildren()) + { + range->set_theme(ctx.theme + ? *ctx.theme + : DesignRegistry::instance().theme(kDesignName)); range->setFixedHeight(60); } return widget; }; #ifdef META_ENABLE_GLM_TYPES registry.add(kDesignName, typeid(glm::vec2), "RangeBar", editor); - registry.add(kDesignName, typeid(std::vector), "PathEditor", editor); - registry.add(kDesignName, typeid(std::vector), "PointsEditor", editor); + registry.add(kDesignName, + typeid(std::vector), + "PathEditor", + editor); + registry.add(kDesignName, + typeid(std::vector), + "PointsEditor", + editor); #endif #ifdef META_ENABLE_ARRAY_TYPES registry.add(kDesignName, typeid(meta::Array), kAnyWidgetType, editor); #endif #ifdef META_ENABLE_COLOR_GRADIENT_TYPES - registry.add(kDesignName, typeid(meta::ColorGradient), kAnyWidgetType, editor); + registry.add(kDesignName, + typeid(meta::ColorGradient), + kAnyWidgetType, + editor); #endif registry.set_fallback(kDesignName, stock::kDesignName); } diff --git a/MetaUI/qt/src/designs/industrial/int_slider.cpp b/MetaUI/qt/src/designs/industrial/int_slider.cpp index 8185bfe..b78e970 100644 --- a/MetaUI/qt/src/designs/industrial/int_slider.cpp +++ b/MetaUI/qt/src/designs/industrial/int_slider.cpp @@ -55,7 +55,20 @@ IntSlider::IntSlider(Attribute &attr, max_ = std::numeric_limits::max(); } - input_max_ = max_ == 64 ? std::numeric_limits::max() : max_; + // The rail may deliberately stop short of what the parameter accepts. Where + // it does, dragging is held to the rail while typing goes to the real + // maximum. Declared per attribute rather than inferred: this used to trigger + // on max == 64 exactly, which caught unrelated parameters whose 64 is a hard + // cap, Islands and n_vertices among them, and let a user type any number + // into them. + input_max_ = max_; + if (const int declared = meta::common::try_get(attr, + meta::keys::ui::drag_max, + 0); + declared > min_ && declared < max_) + { + max_ = declared; // the rail ends here; input_max_ keeps the real limit + } value_ = std::clamp(attr.value(), min_, input_max_); norm_ = unbounded_ ? kRestNorm : to_norm(value_); diff --git a/MetaUI/qt/src/designs/industrial/linked_sliders.cpp b/MetaUI/qt/src/designs/industrial/linked_sliders.cpp index cdabaa0..1ccb5b1 100644 --- a/MetaUI/qt/src/designs/industrial/linked_sliders.cpp +++ b/MetaUI/qt/src/designs/industrial/linked_sliders.cpp @@ -12,7 +12,9 @@ namespace meta::qt::industrial LinkedSliders::LinkedSliders(Attribute &attr, const RowContext &ctx, QWidget *parent) - : Control(ctx, parent), axes_theme_(theme()), state_(&attr.state()) + : Control(ctx, parent), + axes_theme_(theme()), + state_(&attr.state()) { if (auto *flag = state_->try_value(meta::keys::state::locked_xy)) linked_ = *flag; @@ -26,8 +28,8 @@ LinkedSliders::LinkedSliders(Attribute &attr, auto *heading = new QHBoxLayout; auto *label = new QLabel(QString::fromStdString(meta::common::label(attr))); label->setFont(row_label_font()); - label->setStyleSheet( - QString("color: %1; background: transparent;").arg(theme().ink_primary.name())); + label->setStyleSheet(QString("color: %1; background: transparent;") + .arg(theme().ink_primary.name())); heading->addWidget(label, 1); link_ = new QToolButton; link_->setCheckable(true); @@ -39,7 +41,8 @@ LinkedSliders::LinkedSliders(Attribute &attr, link_->setCursor(Qt::PointingHandCursor); link_->setStyleSheet( QString("QToolButton { color: %1; background: %2; border: 1px solid %3; " - "border-radius: 5px; } QToolButton:checked { border-color: %4; background: " + "border-radius: 5px; } QToolButton:checked { border-color: %4; " + "background: " "%5; } QToolButton:hover { border-color: %4; }") .arg(theme().ink_primary.name(), theme().field.name(), @@ -55,22 +58,37 @@ LinkedSliders::LinkedSliders(Attribute &attr, meta::common::min(attr)); metadata.try_add(std::string(meta::keys::constraints::max), meta::common::max(attr)); - metadata.try_add(std::string(meta::keys::ui::label), std::string(i ? "Y" : "X")); - metadata.try_add(std::string(meta::keys::ui::format), std::string("{:.2f}")); + metadata.try_add(std::string(meta::keys::ui::label), + std::string(i ? "Y" : "X")); + metadata.try_add(std::string(meta::keys::ui::format), + std::string("{:.2f}")); + + // Forward the two hints that change how an axis maps and clamps. Without + // them a linked pair silently behaves differently from the single value + // row next to it, which is the failure this control exists to avoid. + if (const auto *p = attr.metadata().try_value( + meta::keys::ui::drag_max)) + metadata.try_add(std::string(meta::keys::ui::drag_max), *p); + + if (const auto *p = attr.metadata().try_value( + meta::keys::ui::log_scale)) + metadata.try_add(std::string(meta::keys::ui::log_scale), *p); RowContext axis_ctx = ctx; axis_ctx.theme = &axes_theme_; if (ctx.default_value) - axis_ctx.default_value = - [get = ctx.default_value, key = attr.name(), i](const std::string &) -> std::any + axis_ctx.default_value = [get = ctx.default_value, key = attr.name(), i]( + const std::string &) -> std::any { const auto initial = get(key); - if (auto pair = std::any_cast(&initial)) - return (*pair)[i]; + if (auto pair = std::any_cast(&initial)) return (*pair)[i]; return {}; }; sliders_[i] = new ParamSlider(axes_[i], axis_ctx, this); layout->addWidget(sliders_[i]); - connect(sliders_[i], &ControlBase::edit_started, this, [this] { begin_edit(); }); + connect(sliders_[i], + &ControlBase::edit_started, + this, + [this] { begin_edit(); }); connect(sliders_[i], &ControlBase::value_changed, this, @@ -84,7 +102,10 @@ LinkedSliders::LinkedSliders(Attribute &attr, } notify_value_changed(); }); - connect(sliders_[i], &ControlBase::edit_ended, this, [this] { end_edit(); }); + connect(sliders_[i], + &ControlBase::edit_ended, + this, + [this] { end_edit(); }); } connect(link_, &QToolButton::toggled, @@ -126,8 +147,7 @@ QSize LinkedSliders::sizeHint() const } void LinkedSliders::on_state_changed() { - if (!link_ || !sliders_[1]) - return; + if (!link_ || !sliders_[1]) return; link_->setText(linked_ ? "X = Y" : "X / Y"); link_->setEnabled(!is_locked()); for (int i = 0; i < 2; ++i) diff --git a/MetaUI/qt/src/designs/industrial/param_slider.cpp b/MetaUI/qt/src/designs/industrial/param_slider.cpp index 128c685..adde5d4 100644 --- a/MetaUI/qt/src/designs/industrial/param_slider.cpp +++ b/MetaUI/qt/src/designs/industrial/param_slider.cpp @@ -63,12 +63,28 @@ ParamSlider::ParamSlider(Attribute &attr, max_ = std::numeric_limits::max(); } - // A log mapping needs a strictly positive lower bound; fall back to linear - // rather than producing NaNs across the whole rail. An unbounded range has - // no span to lay a mapping over in the first place. - if (log_scale_ && (unbounded_ || min_ <= kLogFloor)) log_scale_ = false; - - input_max_ = max_ == 64 ? std::numeric_limits::max() : max_; + // Only an unbounded range defeats a log mapping outright, because there is + // no span to lay one over. A minimum of zero does not: the domain is simply + // clamped up to kLogFloor, which is what stock's SliderFloat has always + // done. Bailing to linear here instead meant every log attribute starting at + // zero silently drew as linear, which is nearly all of them. + if (log_scale_ && unbounded_) log_scale_ = false; + + // The rail may deliberately stop short of what the parameter accepts. Where + // it does, dragging is held to the rail while typing goes to the real + // maximum. Declared per attribute rather than inferred: this used to trigger + // on max == 64 exactly, which caught unrelated parameters whose 64 is a hard + // cap, Islands and n_vertices among them, and let a user type any number + // into them. + input_max_ = max_; + if (const float declared = meta::common::try_get( + attr, + meta::keys::ui::drag_max, + 0.f); + declared > min_ && declared < max_) + { + max_ = declared; // the rail ends here; input_max_ keeps the real limit + } value_ = std::clamp(attr.value(), min_, input_max_); norm_ = unbounded_ ? kRestNorm : to_norm(value_); @@ -116,7 +132,10 @@ ParamSlider::ParamSlider(Attribute &attr, notify_value_changed(); end_edit(); }); - { const QSignalBlocker blocker(glide_); glide_->jump(norm_); } + { + const QSignalBlocker blocker(glide_); + glide_->jump(norm_); + } field_ = new QLineEdit(this); field_->setAlignment(Qt::AlignRight | Qt::AlignVCenter); @@ -193,9 +212,16 @@ qreal ParamSlider::to_norm(float value) const if (log_scale_) { - const qreal lo = std::log(qreal(min_)); - const qreal hi = std::log(qreal(max_)); + // Every endpoint is floored, not just the value. log(0) is negative + // infinity, and a minimum of zero is the common case rather than the + // exotic one, so leaving lo unfloored produced a NaN across the whole rail + // and was why this fell back to linear instead. + const qreal lo = std::log(std::max(qreal(min_), kLogFloor)); + const qreal hi = std::log(std::max(qreal(max_), kLogFloor)); const qreal v = std::log(std::max(qreal(value), kLogFloor)); + + if (hi <= lo) return 0.0; + return std::clamp((v - lo) / (hi - lo), 0.0, 1.0); } @@ -208,8 +234,14 @@ float ParamSlider::from_norm(qreal t) const if (log_scale_) { - const qreal lo = std::log(qreal(min_)); - const qreal hi = std::log(qreal(max_)); + const qreal lo = std::log(std::max(qreal(min_), kLogFloor)); + const qreal hi = std::log(std::max(qreal(max_), kLogFloor)); + + // Snap the bottom of the rail back to the real minimum. The floor is a + // device for making the mapping well defined, and without this a rail + // declared from zero would bottom out at 1e-6 instead of at zero. + if (t <= 0.0) return min_; + return float(std::exp(lo + t * (hi - lo))); } @@ -480,7 +512,8 @@ void ParamSlider::commit_value(float value) if (!unbounded_) { // Typed numbers are authoritative, even where normalising a wide range - // cannot represent all their digits. Position the rail, then seat the value. + // cannot represent all their digits. Position the rail, then seat the + // value. glide_->jump(to_norm(clamped)); } diff --git a/MetaUI/qt/src/designs/industrial/text_row.cpp b/MetaUI/qt/src/designs/industrial/text_row.cpp index 4792a55..2481a28 100644 --- a/MetaUI/qt/src/designs/industrial/text_row.cpp +++ b/MetaUI/qt/src/designs/industrial/text_row.cpp @@ -14,12 +14,16 @@ namespace meta::qt::industrial { -TextRow::TextRow(Attribute &attr, const RowContext &ctx, QWidget *parent) +TextRow::TextRow(Attribute &attr, + const RowContext &ctx, + QWidget *parent) : Control(ctx, parent) { label_ = meta::common::label(attr); value_ = attr.value(); - read_only_ = meta::common::try_get(attr, meta::keys::ui::read_only, false) || + read_only_ = meta::common::try_get(attr, + meta::keys::ui::read_only, + false) || meta::common::widget_type(attr) == "ReadOnlyText"; setFixedHeight(theme().metrics.row_height); @@ -44,8 +48,7 @@ TextRow::TextRow(Attribute &attr, const RowContext &ctx, QWidget *p [this]() { const std::string typed = field_->text().toStdString(); - if (typed == value_) - return; + if (typed == value_) return; begin_edit(); value_ = typed; @@ -53,7 +56,10 @@ TextRow::TextRow(Attribute &attr, const RowContext &ctx, QWidget *p end_edit(); }); - connect(field_, &QLineEdit::textEdited, this, [this]() { restyle_field(true); }); + connect(field_, + &QLineEdit::textEdited, + this, + [this]() { restyle_field(true); }); } bool TextRow::can_render(const Attribute &) { return true; } @@ -67,7 +73,8 @@ void TextRow::set(const std::string &value) QSize TextRow::sizeHint() const { - return QSize(theme().metrics.label_min_width + 160, theme().metrics.row_height); + return QSize(theme().metrics.label_min_width + 160, + theme().metrics.row_height); } // --- geometry @@ -106,10 +113,11 @@ void TextRow::paintEvent(QPaintEvent *) QFont label_font = row_label_font(); painter.setFont(label_font); painter.setPen(theme().state_ink(is_modified(), is_locked() || read_only_)); - painter.drawText( - label_rect(), - Qt::AlignLeft | Qt::AlignVCenter, - elide_label(QString::fromStdString(label_), label_font, label_rect().width())); + painter.drawText(label_rect(), + Qt::AlignLeft | Qt::AlignVCenter, + elide_label(QString::fromStdString(label_), + label_font, + label_rect().width())); } void TextRow::resizeEvent(QResizeEvent *event) @@ -132,8 +140,7 @@ bool TextRow::eventFilter(QObject *watched, QEvent *event) (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut)) { const bool editing = event->type() == QEvent::FocusIn; - if (!editing) - refresh_field(); + if (!editing) refresh_field(); restyle_field(editing); } @@ -144,8 +151,7 @@ bool TextRow::eventFilter(QObject *watched, QEvent *event) void TextRow::refresh_field() { - if (!field_ || field_->hasFocus()) - return; // never overwrite mid-typing + if (!field_ || field_->hasFocus()) return; // never overwrite mid-typing const QSignalBlocker blocker(field_); field_->setText(QString::fromStdString(value_)); @@ -153,8 +159,7 @@ void TextRow::refresh_field() void TextRow::restyle_field(bool editing) { - if (!field_) - return; + if (!field_) return; const bool locked = is_locked() || read_only_; diff --git a/MetaUI/qt/src/widgets/array_canvas.cpp b/MetaUI/qt/src/widgets/array_canvas.cpp index 681a89d..a05c162 100644 --- a/MetaUI/qt/src/widgets/array_canvas.cpp +++ b/MetaUI/qt/src/widgets/array_canvas.cpp @@ -41,10 +41,7 @@ ArrayCanvas::ArrayCanvas(const std::string &label, update_geometry(); } -QSize ArrayCanvas::sizeHint() const -{ - return QSize(320, 320); -} +QSize ArrayCanvas::sizeHint() const { return QSize(320, 320); } void ArrayCanvas::set_field_data(const std::vector &data) { @@ -157,7 +154,6 @@ void ArrayCanvas::draw_at(const QPoint &pos, Qt::MouseButtons buttons) } } } - } QColor ArrayCanvas::colormap(float v) const @@ -185,16 +181,16 @@ void ArrayCanvas::update_geometry() void ArrayCanvas::resizeEvent(QResizeEvent *event) { - if (height() != width()) - setFixedHeight(width()); + if (height() != width()) setFixedHeight(width()); update_geometry(); QWidget::resizeEvent(event); } QPoint ArrayCanvas::field_position(const QPoint &pos) const { - return QPoint(int(double(pos.x() - rect_img_.x()) * width_ / rect_img_.width()), - int(double(pos.y() - rect_img_.y()) * height_ / rect_img_.height())); + return QPoint( + int(double(pos.x() - rect_img_.x()) * width_ / rect_img_.width()), + int(double(pos.y() - rect_img_.y()) * height_ / rect_img_.height())); } bool ArrayCanvas::event(QEvent *event) @@ -306,8 +302,10 @@ void ArrayCanvas::mouseMoveEvent(QMouseEvent *event) { const QPoint pos = field_position(event->position().toPoint()); const QPoint delta = pos - pos_previous_; - const int steps = std::max(1, int(std::ceil(std::hypot(delta.x(), delta.y()) / - std::max(1., brush_radius_ / 3.)))); + const int steps = std::max( + 1, + int(std::ceil(std::hypot(delta.x(), delta.y()) / + std::max(1., brush_radius_ / 3.)))); for (int i = 1; i <= steps; ++i) draw_at(pos_previous_ + QPoint(qRound(double(delta.x()) * i / steps), qRound(double(delta.y()) * i / steps)), @@ -405,8 +403,8 @@ void ArrayCanvas::paintEvent(QPaintEvent *) painter.setPen(pen); painter.setBrush(Qt::NoBrush); painter.drawEllipse(QPointF(mouse_pos), - double(brush_radius_) * rect_img_.width() / width_, - double(brush_radius_) * rect_img_.height() / height_); + double(brush_radius_) * rect_img_.width() / width_, + double(brush_radius_) * rect_img_.height() / height_); // Info overlay QString txt; diff --git a/MetaUI/qt/src/widgets/gradient_picker.cpp b/MetaUI/qt/src/widgets/gradient_picker.cpp index 64e0495..05fd24d 100644 --- a/MetaUI/qt/src/widgets/gradient_picker.cpp +++ b/MetaUI/qt/src/widgets/gradient_picker.cpp @@ -361,14 +361,19 @@ class PresetGridWidget : public QWidget if (buttons_.empty()) return; const int cols = compute_cols(avail_w); - const int tile_width = std::max(swatch_w_, (avail_w - 4 - (cols - 1) * spacing_) / cols); + const int tile_width = std::max(swatch_w_, + (avail_w - 4 - (cols - 1) * spacing_) / + cols); for (auto *button : buttons_) { button->setFixedWidth(tile_width); const auto source = button->property("swatch_image").value(); - if (!source.isNull()) { + if (!source.isNull()) + { const QSize icon_size(tile_width - 6, swatch_h_ - 6); - button->setIcon(QIcon(source.scaled(icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation))); + button->setIcon(QIcon(source.scaled(icon_size, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation))); button->setIconSize(icon_size); } } @@ -580,8 +585,14 @@ QWidget *GradientPicker::build_toolbar() auto *files = make_button(tr("Files ▾"), tr("Import or export gradients")); auto *menu = new QMenu(files); menu->addAction(tr("Import…"), import_button_, &QToolButton::click); - auto *export_action = menu->addAction(tr("Export library…"), export_button_, &QToolButton::click); - connect(menu, &QMenu::aboutToShow, this, [this, export_action] { export_action->setEnabled(export_button_->isEnabled()); }); + auto *export_action = menu->addAction(tr("Export library…"), + export_button_, + &QToolButton::click); + connect(menu, + &QMenu::aboutToShow, + this, + [this, export_action] + { export_action->setEnabled(export_button_->isEnabled()); }); files->setMenu(menu); files->setPopupMode(QToolButton::InstantPopup); import_button_->hide(); @@ -709,7 +720,7 @@ void GradientPicker::rebuild_entries() QPixmap GradientPicker::make_swatch(const Entry &entry, bool favorite) const { - QPixmap pix(SWATCH_W, SWATCH_H); + QPixmap pix(SWATCH_W, SWATCH_H); pix.fill(Qt::transparent); QPainter pp(&pix); pp.setRenderHint(QPainter::Antialiasing); @@ -724,14 +735,18 @@ QPixmap GradientPicker::make_swatch(const Entry &entry, bool favorite) const // Name overlay const bool generated_name = entry.preset.name.size() == 6 && - std::all_of(entry.preset.name.begin(), entry.preset.name.end(), - [](unsigned char c) { return std::isxdigit(c); }); - if (!generated_name) { - pp.fillRect(QRect(0, pix.height() - 13, pix.width(), 13), QColor(0, 0, 0, 150)); - pp.setPen(Qt::white); - pp.setFont(QFont(pp.font().family(), 7)); - pp.drawText(pix.rect().adjusted(2, 0, -2, 0), - Qt::AlignBottom | Qt::AlignHCenter, + std::all_of(entry.preset.name.begin(), + entry.preset.name.end(), + [](unsigned char c) + { return std::isxdigit(c); }); + if (!generated_name) + { + pp.fillRect(QRect(0, pix.height() - 13, pix.width(), 13), + QColor(0, 0, 0, 150)); + pp.setPen(Qt::white); + pp.setFont(QFont(pp.font().family(), 7)); + pp.drawText(pix.rect().adjusted(2, 0, -2, 0), + Qt::AlignBottom | Qt::AlignHCenter, QString::fromStdString(entry.preset.name)); } diff --git a/MetaUI/qt/src/widgets/points_canvas.cpp b/MetaUI/qt/src/widgets/points_canvas.cpp index fc8c692..0638154 100644 --- a/MetaUI/qt/src/widgets/points_canvas.cpp +++ b/MetaUI/qt/src/widgets/points_canvas.cpp @@ -5,11 +5,11 @@ #include #include +#include "meta_qt/ui/number_format.hpp" #include #include #include #include -#include "meta_qt/ui/number_format.hpp" #include #include #include @@ -24,8 +24,7 @@ namespace meta::qt void PointsCanvas::resizeEvent(QResizeEvent *event) { - if (height() != width()) - setFixedHeight(width()); + if (height() != width()) setFixedHeight(width()); QWidget::resizeEvent(event); } @@ -41,11 +40,13 @@ void PointsCanvas::keyPressEvent(QKeyEvent *event) { if (order_input_.size() < 9) order_input_ += event->text(); } - else if (event->key() == Qt::Key_Backspace) order_input_.chop(1); - else if (event->key() == Qt::Key_Escape) order_input_.clear(); + else if (event->key() == Qt::Key_Backspace) + order_input_.chop(1); + else if (event->key() == Qt::Key_Escape) + order_input_.clear(); else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { - bool valid = false; + bool valid = false; const int destination = order_input_.toInt(&valid) - 1; if (valid && destination >= 0 && destination < int(points_.size())) { @@ -58,7 +59,11 @@ void PointsCanvas::keyPressEvent(QKeyEvent *event) Q_EMIT drag_ended(); } } - else { QWidget::keyPressEvent(event); return; } + else + { + QWidget::keyPressEvent(event); + return; + } event->accept(); update(); } @@ -88,7 +93,8 @@ PointsCanvas::PointsCanvas(std::vector &points, setSizePolicy(policy); setMouseTracking(true); setFocusPolicy(Qt::StrongFocus); - setToolTip(tr("Hover a point and scroll to change its height. For paths, type its new position and press Enter. Escape cancels.")); + setToolTip(tr("Hover a point and scroll to change its height. For paths, " + "type its new position and press Enter. Escape cancels.")); setCursor(Qt::CrossCursor); } @@ -398,22 +404,27 @@ void PointsCanvas::paintEvent(QPaintEvent *) { p.setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); p.setPen(palette().color(QPalette::Text)); - const QString info = order_input_.isEmpty() - ? tr("Point %1 · Height %2").arg(i + 1).arg(display_float(pt.z)) - : tr("Move to %1 · Enter to apply").arg(order_input_); - p.fillRect(r.adjusted(0, r.height() - 30, 0, 0), palette().color(QPalette::Base)); - p.drawText(r.adjusted(6, 0, -6, -10), Qt::AlignLeft | Qt::AlignBottom, info); + const QString info = + order_input_.isEmpty() + ? tr("Point %1 · Height %2").arg(i + 1).arg(display_float(pt.z)) + : tr("Move to %1 · Enter to apply").arg(order_input_); + p.fillRect(r.adjusted(0, r.height() - 30, 0, 0), + palette().color(QPalette::Base)); + p.drawText(r.adjusted(6, 0, -6, -10), + Qt::AlignLeft | Qt::AlignBottom, + info); } } // Point count p.setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); p.setPen(palette().color(QPalette::PlaceholderText)); - if (hovered_idx_ < 0 && drag_idx_ < 0) p.drawText(r.adjusted(4, 0, 0, -3), - Qt::AlignLeft | Qt::AlignBottom, - QString("%1 pt%2") - .arg(points_.size()) - .arg(points_.size() != 1 ? "s" : "")); + if (hovered_idx_ < 0 && drag_idx_ < 0) + p.drawText(r.adjusted(4, 0, 0, -3), + Qt::AlignLeft | Qt::AlignBottom, + QString("%1 pt%2") + .arg(points_.size()) + .arg(points_.size() != 1 ? "s" : "")); // Legend: colour ramp strip bottom-right { @@ -546,7 +557,9 @@ void PointsCanvas::wheelEvent(QWheelEvent *e) QColor PointsCanvas::z_to_color(float z) const { if (property("industrialEditor").toBool()) - return palette().color(QPalette::Highlight).lighter(70 + int(60 * std::clamp(z, 0.f, 1.f))); + return palette() + .color(QPalette::Highlight) + .lighter(70 + int(60 * std::clamp(z, 0.f, 1.f))); // Simple blue(0) → cyan → green → yellow → red(1) heatmap. z = std::clamp(z, 0.f, 1.f); float r, g, b; diff --git a/MetaUI/qt/src/widgets/range_bar.cpp b/MetaUI/qt/src/widgets/range_bar.cpp index 7141845..6951a51 100644 --- a/MetaUI/qt/src/widgets/range_bar.cpp +++ b/MetaUI/qt/src/widgets/range_bar.cpp @@ -3,14 +3,14 @@ this software. */ #include +#include "meta_qt/ui/number_format.hpp" +#include "meta_qt/ui/theme.hpp" #include +#include #include #include #include #include -#include -#include "meta_qt/ui/number_format.hpp" -#include "meta_qt/ui/theme.hpp" #include "meta_qt/widgets/range_bar.hpp" @@ -181,7 +181,8 @@ void RangeBar::paintEvent(QPaintEvent *) if (hx > lx) { QRect filled(lx, tr.top(), hx - lx, tr.height()); - p.setBrush(industrial_ ? theme_.rail_fill("", !isEnabled()) : palette().color(QPalette::Highlight).darker(110)); + p.setBrush(industrial_ ? theme_.rail_fill("", !isEnabled()) + : palette().color(QPalette::Highlight).darker(110)); p.drawRect(filled); } @@ -277,8 +278,11 @@ void RangeBar::paintEvent(QPaintEvent *) { if (industrial_) { - const auto &m = theme_.metrics; - const QRectF thumb(x - m.thumb_width / 2., tr.center().y() - m.thumb_height / 2., m.thumb_width, m.thumb_height); + const auto &m = theme_.metrics; + const QRectF thumb(x - m.thumb_width / 2., + tr.center().y() - m.thumb_height / 2., + m.thumb_width, + m.thumb_height); QLinearGradient metal(thumb.topLeft(), thumb.bottomLeft()); metal.setColorAt(0, theme_.thumb_top); metal.setColorAt(1, theme_.thumb_bottom); @@ -319,10 +323,9 @@ void RangeBar::paintEvent(QPaintEvent *) lo_txt); // High label — right-aligned below the high handle - p.drawText( - QRect(tr.center().x(), tr.bottom() + 5, tr.width() / 2, 20), - Qt::AlignRight | Qt::AlignTop, - hi_txt); + p.drawText(QRect(tr.center().x(), tr.bottom() + 5, tr.width() / 2, 20), + Qt::AlignRight | Qt::AlignTop, + hi_txt); } void RangeBar::set_value(glm::vec2 v) diff --git a/tests/test_qt/test_editor_controls/main.cpp b/tests/test_qt/test_editor_controls/main.cpp index 5cc3f6d..4c0a3bf 100644 --- a/tests/test_qt/test_editor_controls/main.cpp +++ b/tests/test_qt/test_editor_controls/main.cpp @@ -1,158 +1,265 @@ +#include "meta/ext/array/array.hpp" +#include "meta/ext/color_gradient/color_gradient.hpp" +#include "meta/ext/color_gradient/gradient_library.hpp" +#include "meta_qt/designs/industrial/industrial.hpp" +#include "meta_qt/designs/industrial/int_slider.hpp" +#include "meta_qt/designs/industrial/linked_sliders.hpp" +#include "meta_qt/designs/industrial/param_slider.hpp" +#include "meta_qt/designs/industrial/section.hpp" +#include "meta_qt/ui/design_registry.hpp" +#include "meta_qt/ui/number_format.hpp" +#include "meta_qt/widgets/array_canvas.hpp" +#include "meta_qt/widgets/points_canvas.hpp" +#include "meta_qt/widgets/range_bar.hpp" #include #include +#include #include #include -#include #include #include #include #include #include #include -#include "meta_qt/ui/design_registry.hpp" -#include "meta_qt/ui/number_format.hpp" -#include "meta_qt/designs/industrial/industrial.hpp" -#include "meta_qt/designs/industrial/linked_sliders.hpp" -#include "meta_qt/designs/industrial/param_slider.hpp" -#include "meta_qt/designs/industrial/int_slider.hpp" -#include "meta_qt/designs/industrial/section.hpp" -#include "meta_qt/widgets/array_canvas.hpp" -#include "meta_qt/widgets/points_canvas.hpp" -#include "meta_qt/widgets/range_bar.hpp" -#include "meta/ext/array/array.hpp" -#include "meta/ext/color_gradient/color_gradient.hpp" -#include "meta/ext/color_gradient/gradient_library.hpp" using namespace meta::qt; -namespace { -int failures=0; -void check(bool ok, const char *message) { if (!ok) { ++failures; std::cerr << message << '\n'; } } -void flush() { for(int i=0;i<8;++i) { QApplication::sendPostedEvents(); QApplication::processEvents(); } } -template void describe(meta::Attribute &a,const char *kind,const char *label) { - a.metadata().add(meta::keys::ui::widget_type,std::string(kind)); - a.metadata().add(meta::keys::ui::label,std::string(label)); +namespace +{ +int failures = 0; +void check(bool ok, const char *message) +{ + if (!ok) + { + ++failures; + std::cerr << message << '\n'; + } +} +void flush() +{ + for (int i = 0; i < 8; ++i) + { + QApplication::sendPostedEvents(); + QApplication::processEvents(); + } } -void mouse(QWidget *w,QEvent::Type type,QPoint pos) { - QMouseEvent event(type,QPointF(pos),QPointF(w->mapToGlobal(pos)),Qt::LeftButton,Qt::LeftButton,Qt::NoModifier); - QApplication::sendEvent(w,&event); +template +void describe(meta::Attribute &a, const char *kind, const char *label) +{ + a.metadata().add(meta::keys::ui::widget_type, std::string(kind)); + a.metadata().add(meta::keys::ui::label, std::string(label)); } +void mouse(QWidget *w, QEvent::Type type, QPoint pos) +{ + QMouseEvent event(type, + QPointF(pos), + QPointF(w->mapToGlobal(pos)), + Qt::LeftButton, + Qt::LeftButton, + Qt::NoModifier); + QApplication::sendEvent(w, &event); } -int main(int argc,char **argv) { - QApplication app(argc,argv); - Theme theme; - theme.accent = QColor("#5f85ab"); - QPalette palette = app.palette(); - palette.setColor(QPalette::Window, theme.section_surface); - palette.setColor(QPalette::WindowText, theme.ink_primary); - app.setPalette(palette); - RowContext ctx; ctx.theme=&theme; - industrial::register_design(); - auto ®istry=DesignRegistry::instance(); - { - meta::Attribute scalar("scalar",512.f); - describe(scalar,"Slider","Scalar"); - scalar.metadata().add(meta::keys::constraints::min,0.f); - scalar.metadata().add(meta::keys::constraints::max,64.f); - industrial::ParamSlider slider(scalar,ctx); - check(slider.get()==512.f,"initial float above drag range was lost"); - slider.set(1024.f); - check(slider.get()==1024.f,"float above drag range was lost on refresh"); - auto *field=slider.findChild(); - field->setText("512"); QMetaObject::invokeMethod(field,"editingFinished"); - check(slider.get()==512.f,"typed float capped at drag limit"); - slider.resize(400,slider.height()); slider.show(); flush(); - mouse(&slider,QEvent::MouseButtonPress,QPoint(295,slider.height()/2)); - mouse(&slider,QEvent::MouseMove,QPoint(800,slider.height()/2)); - mouse(&slider,QEvent::MouseButtonRelease,QPoint(800,slider.height()/2)); - check(slider.get()<=64.f,"float drag exceeded 64"); - meta::Attribute integer("integer",512); - describe(integer,"Slider","Integer"); - integer.metadata().add(meta::keys::constraints::min,0); - integer.metadata().add(meta::keys::constraints::max,64); - industrial::IntSlider ints(integer,ctx); - check(ints.get()==512,"initial integer above drag range was lost"); - ints.set(1024); check(ints.get()==1024,"integer refresh capped at drag limit"); - auto *int_field=ints.findChild(); - int_field->setText("512"); QMetaObject::invokeMethod(int_field,"editingFinished"); - check(ints.get()==512,"typed integer capped at drag limit"); - } - check(display_float(.001f)=="0.001", "small float displayed as zero"); - check(display_float(-.0001f)=="-0.0001", "negative small float lost precision"); - check(display_float(2.f)=="2.00", "ordinary float must retain two decimals"); - check(display_float(.00125f)=="0.00125", "small fractional detail lost"); - for(bool bounded:{false,true}) { - meta::Attribute frequency("frequency",glm::vec2(4,5)); - describe(frequency,"LinkedSliders","Spatial Frequency"); - if(bounded) { frequency.metadata().add(meta::keys::constraints::min,0.f); frequency.metadata().add(meta::keys::constraints::max,100.f); } - std::unique_ptr row(registry.render(&frequency,"industrial",ctx)); - check(row->findChild()!=nullptr,"frequency fell back to stock"); - auto fields=row->findChildren(); - check(fields.size()==2,"linked pair must expose two fields"); - fields[0]->setText("0.001"); QMetaObject::invokeMethod(fields[0],"editingFinished"); - check(frequency.value().x==.001f,"typed float not committed exactly"); - auto *link=row->findChild(); link->click(); - check(frequency.value().y==frequency.value().x,"link must copy X to Y"); - fields[0]->setText("0.0001"); QMetaObject::invokeMethod(fields[0],"editingFinished"); - check(frequency.value().x==.0001f && frequency.value().y==.0001f,"linked precision lost"); - link->click(); fields[1]->setText("0.002"); QMetaObject::invokeMethod(fields[1],"editingFinished"); - check(frequency.value().x==.0001f && frequency.value().y==.002f,"unlinked axes not independent"); - } - meta::Attribute> path("path",{{.15f,.3f,.7f},{.7f,.7f,1.f},{.75f,.2f,.5f}}); - describe(path,"PathEditor","Path"); - meta::Attribute brush("brush",meta::Array{{256,256},std::vector(256*256,0)}); - describe(brush,"ArrayEditor","Brush"); - brush.metadata().add(meta::keys::ui::width,256); brush.metadata().add(meta::keys::ui::height,256); - meta::Attribute frequency("frequency",glm::vec2(4)); describe(frequency,"LinkedSliders","Spatial Frequency"); - frequency.state().add(meta::keys::state::locked_xy,true); - meta::Attribute range("range",glm::vec2(.2f,.8f)); describe(range,"RangeBar","Remap Range"); - range.metadata().add(meta::keys::constraints::min,0.f); range.metadata().add(meta::keys::constraints::max,1.f); - range.state().add(meta::keys::state::active,true); - meta::Attribute gradient("gradient",meta::ColorGradient{}); describe(gradient,"GradientEditor","Gradient"); - meta::GradientPresets presets; - for(int i=0;i<20;++i) presets.presets.push_back({"Preset "+std::to_string(i), {{0,{0.f,float(i)/20,.2f,1}}, {1,{1,1,1,1}}}}); - gradient.metadata().add(meta::keys::ui::presets, presets); - meta::GradientLibrary::instance().set_path(std::filesystem::path(QDir::tempPath().toStdString())/"hesiod-editor-check-gradients.json"); - QScrollArea scroll; scroll.setAttribute(Qt::WA_DontShowOnScreen); scroll.setWidgetResizable(true); - auto *page=new QWidget; auto *layout=new QVBoxLayout(page); layout->setContentsMargins(0,0,0,0); layout->setSpacing(2); layout->setAlignment(Qt::AlignTop); - std::vector rows; - for(auto *attr:std::vector{&frequency,&path,&brush,&range,&gradient}) { - auto *section=new industrial::Section("Parameters",theme); - auto *row=registry.render(attr,"industrial",ctx); - rows.push_back(row); section->content_layout->addWidget(row); section->set_expanded(true); layout->addWidget(section); - } - scroll.setWidget(page); scroll.resize(440,720); scroll.show(); flush(); - auto *points=page->findChild(); auto *paint=page->findChild(); - { - const auto original = path.value(); - const int side=points->width()-20; - mouse(points,QEvent::MouseMove,QPoint(10+int(.15f*side),points->height()-11-int(.3f*side))); - QKeyEvent digit(QEvent::KeyPress,Qt::Key_3,Qt::NoModifier,"3"); - QApplication::sendEvent(points,&digit); - QKeyEvent enter(QEvent::KeyPress,Qt::Key_Return,Qt::NoModifier); - QApplication::sendEvent(points,&enter); - check(path.value()[2]==original[0] && path.value()[0]==original[1],"keyboard path reorder failed or changed point data"); - } - for(int width:{240,360,520,360,240,520}) { - scroll.resize(width,720); flush(); - std::cout << "width="<width()==paint->height(),"Brush canvas is not square"); - } - const QPoint begin(paint->width()/4,paint->height()/2), end(paint->width()*3/4,paint->height()/2); - mouse(paint,QEvent::MouseButtonPress,begin); mouse(paint,QEvent::MouseMove,end); mouse(paint,QEvent::MouseButtonRelease,end); - check(paint->get_field_data()[128*256+128]>0,"fast brush stroke has a gap"); - check(brush.value().shape==glm::ivec2(256),"display resize changed paint resolution"); - auto buttons=rows[3]->findChildren(); - QPushButton *toggle=nullptr; - for(auto *button:buttons) if(button->isCheckable()) toggle=button; - toggle->click(); - check(!rows[3]->findChild()->isEnabled(),"range toggle did not disable range"); - toggle->click(); - check(range.value()==glm::vec2(.2f,.8f),"range toggle lost saved endpoints"); - if(argc>1) { - scroll.resize(440,720); flush(); - QDir out(QString::fromLocal8Bit(argv[1])); out.mkpath("."); - int i=0; for(auto *row:rows) row->grab().save(out.filePath(QString("editor-%1.png").arg(i++))); - } - std::cout << "editor checks: failures="< scalar("scalar", 512.f); + describe(scalar, "Slider", "Scalar"); + // The rail ends at 64, the parameter accepts far more. Declared through + // ui.drag_max now: this used to be inferred from a maximum of exactly 64, + // which caught unrelated parameters whose 64 is a hard cap. + scalar.metadata().add(meta::keys::constraints::min, 0.f); + scalar.metadata().add(meta::keys::constraints::max, 4096.f); + scalar.metadata().add(meta::keys::ui::drag_max, 64.f); + industrial::ParamSlider slider(scalar, ctx); + check(slider.get() == 512.f, "initial float above drag range was lost"); + slider.set(1024.f); + check(slider.get() == 1024.f, "float above drag range was lost on refresh"); + auto *field = slider.findChild(); + field->setText("512"); + QMetaObject::invokeMethod(field, "editingFinished"); + check(slider.get() == 512.f, "typed float capped at drag limit"); + slider.resize(400, slider.height()); + slider.show(); + flush(); + mouse(&slider, QEvent::MouseButtonPress, QPoint(295, slider.height() / 2)); + mouse(&slider, QEvent::MouseMove, QPoint(800, slider.height() / 2)); + mouse(&slider, + QEvent::MouseButtonRelease, + QPoint(800, slider.height() / 2)); + check(slider.get() <= 64.f, "float drag exceeded 64"); + meta::Attribute integer("integer", 512); + describe(integer, "Slider", "Integer"); + integer.metadata().add(meta::keys::constraints::min, 0); + integer.metadata().add(meta::keys::constraints::max, 4096); + integer.metadata().add(meta::keys::ui::drag_max, 64); + industrial::IntSlider ints(integer, ctx); + check(ints.get() == 512, "initial integer above drag range was lost"); + ints.set(1024); + check(ints.get() == 1024, "integer refresh capped at drag limit"); + auto *int_field = ints.findChild(); + int_field->setText("512"); + QMetaObject::invokeMethod(int_field, "editingFinished"); + check(ints.get() == 512, "typed integer capped at drag limit"); + } + check(display_float(.001f) == "0.001", "small float displayed as zero"); + check(display_float(-.0001f) == "-0.0001", + "negative small float lost precision"); + check(display_float(2.f) == "2.00", + "ordinary float must retain two decimals"); + check(display_float(.00125f) == "0.00125", "small fractional detail lost"); + for (bool bounded : {false, true}) + { + meta::Attribute frequency("frequency", glm::vec2(4, 5)); + describe(frequency, "LinkedSliders", "Spatial Frequency"); + if (bounded) + { + frequency.metadata().add(meta::keys::constraints::min, 0.f); + frequency.metadata().add(meta::keys::constraints::max, 100.f); + } + std::unique_ptr row( + registry.render(&frequency, "industrial", ctx)); + check(row->findChild() != nullptr, + "frequency fell back to stock"); + auto fields = row->findChildren(); + check(fields.size() == 2, "linked pair must expose two fields"); + fields[0]->setText("0.001"); + QMetaObject::invokeMethod(fields[0], "editingFinished"); + check(frequency.value().x == .001f, "typed float not committed exactly"); + auto *link = row->findChild(); + link->click(); + check(frequency.value().y == frequency.value().x, "link must copy X to Y"); + fields[0]->setText("0.0001"); + QMetaObject::invokeMethod(fields[0], "editingFinished"); + check(frequency.value().x == .0001f && frequency.value().y == .0001f, + "linked precision lost"); + link->click(); + fields[1]->setText("0.002"); + QMetaObject::invokeMethod(fields[1], "editingFinished"); + check(frequency.value().x == .0001f && frequency.value().y == .002f, + "unlinked axes not independent"); + } + meta::Attribute> path( + "path", + {{.15f, .3f, .7f}, {.7f, .7f, 1.f}, {.75f, .2f, .5f}}); + describe(path, "PathEditor", "Path"); + meta::Attribute brush( + "brush", + meta::Array{{256, 256}, std::vector(256 * 256, 0)}); + describe(brush, "ArrayEditor", "Brush"); + brush.metadata().add(meta::keys::ui::width, 256); + brush.metadata().add(meta::keys::ui::height, 256); + meta::Attribute frequency("frequency", glm::vec2(4)); + describe(frequency, "LinkedSliders", "Spatial Frequency"); + frequency.state().add(meta::keys::state::locked_xy, true); + meta::Attribute range("range", glm::vec2(.2f, .8f)); + describe(range, "RangeBar", "Remap Range"); + range.metadata().add(meta::keys::constraints::min, 0.f); + range.metadata().add(meta::keys::constraints::max, 1.f); + range.state().add(meta::keys::state::active, true); + meta::Attribute gradient("gradient", + meta::ColorGradient{}); + describe(gradient, "GradientEditor", "Gradient"); + meta::GradientPresets presets; + for (int i = 0; i < 20; ++i) + presets.presets.push_back( + {"Preset " + std::to_string(i), + {{0, {0.f, float(i) / 20, .2f, 1}}, {1, {1, 1, 1, 1}}}}); + gradient.metadata().add(meta::keys::ui::presets, presets); + meta::GradientLibrary::instance().set_path( + std::filesystem::path(QDir::tempPath().toStdString()) / + "hesiod-editor-check-gradients.json"); + QScrollArea scroll; + scroll.setAttribute(Qt::WA_DontShowOnScreen); + scroll.setWidgetResizable(true); + auto *page = new QWidget; + auto *layout = new QVBoxLayout(page); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(2); + layout->setAlignment(Qt::AlignTop); + std::vector rows; + for (auto *attr : std::vector{&frequency, + &path, + &brush, + &range, + &gradient}) + { + auto *section = new industrial::Section("Parameters", theme); + auto *row = registry.render(attr, "industrial", ctx); + rows.push_back(row); + section->content_layout->addWidget(row); + section->set_expanded(true); + layout->addWidget(section); + } + scroll.setWidget(page); + scroll.resize(440, 720); + scroll.show(); + flush(); + auto *points = page->findChild(); + auto *paint = page->findChild(); + { + const auto original = path.value(); + const int side = points->width() - 20; + mouse( + points, + QEvent::MouseMove, + QPoint(10 + int(.15f * side), points->height() - 11 - int(.3f * side))); + QKeyEvent digit(QEvent::KeyPress, Qt::Key_3, Qt::NoModifier, "3"); + QApplication::sendEvent(points, &digit); + QKeyEvent enter(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); + QApplication::sendEvent(points, &enter); + check(path.value()[2] == original[0] && path.value()[0] == original[1], + "keyboard path reorder failed or changed point data"); + } + for (int width : {240, 360, 520, 360, 240, 520}) + { + scroll.resize(width, 720); + flush(); + std::cout << "width=" << width << " path=" << points->width() << 'x' + << points->height() << " brush=" << paint->width() << 'x' + << paint->height() << '\n'; + check(points->width() == points->height(), "Path canvas is not square"); + check(paint->width() == paint->height(), "Brush canvas is not square"); + } + const QPoint begin(paint->width() / 4, paint->height() / 2), + end(paint->width() * 3 / 4, paint->height() / 2); + mouse(paint, QEvent::MouseButtonPress, begin); + mouse(paint, QEvent::MouseMove, end); + mouse(paint, QEvent::MouseButtonRelease, end); + check(paint->get_field_data()[128 * 256 + 128] > 0, + "fast brush stroke has a gap"); + check(brush.value().shape == glm::ivec2(256), + "display resize changed paint resolution"); + auto buttons = rows[3]->findChildren(); + QPushButton *toggle = nullptr; + for (auto *button : buttons) + if (button->isCheckable()) toggle = button; + toggle->click(); + check(!rows[3]->findChild()->isEnabled(), + "range toggle did not disable range"); + toggle->click(); + check(range.value() == glm::vec2(.2f, .8f), + "range toggle lost saved endpoints"); + if (argc > 1) + { + scroll.resize(440, 720); + flush(); + QDir out(QString::fromLocal8Bit(argv[1])); + out.mkpath("."); + int i = 0; + for (auto *row : rows) + row->grab().save(out.filePath(QString("editor-%1.png").arg(i++))); + } + std::cout << "editor checks: failures=" << failures << '\n'; + return failures ? 1 : 0; } diff --git a/tests/test_qt/test_slider_mapping/CMakeLists.txt b/tests/test_qt/test_slider_mapping/CMakeLists.txt new file mode 100644 index 0000000..e66fb80 --- /dev/null +++ b/tests/test_qt/test_slider_mapping/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(test_slider_mapping main.cpp) +target_link_libraries(test_slider_mapping meta meta_qt) diff --git a/tests/test_qt/test_slider_mapping/main.cpp b/tests/test_qt/test_slider_mapping/main.cpp new file mode 100644 index 0000000..2a4357c --- /dev/null +++ b/tests/test_qt/test_slider_mapping/main.cpp @@ -0,0 +1,213 @@ +// Regression tests for two industrial slider behaviours. +// +// 1. log_scale (Meta issue #55). The mapping used to be discarded whenever the +// minimum sat at or below the log floor, which includes the very common +// minimum of zero, so a log attribute drew and dragged as a linear one and +// nothing said why. +// +// 2. ui.drag_max. The rail may deliberately stop short of what the parameter +// accepts: dragging is held to the rail, typing goes to the real maximum. +// This used to key off a maximum of exactly 64, which caught unrelated +// parameters whose 64 is a hard cap rather than a comfortable rail end. + +#include +#include +#include + +#include +#include +#include + +#include "meta/core/attribute_container.hpp" +#include "meta/metadata/keys.hpp" + +#include "meta_qt/designs/industrial/param_slider.hpp" + +using namespace meta::qt; + +namespace +{ + +int failures = 0; + +void check(bool ok, const char *message) +{ + if (!ok) + { + ++failures; + std::cerr << "FAIL: " << message << '\n'; + } +} + +void flush() +{ + for (int i = 0; i < 8; ++i) + { + QApplication::sendPostedEvents(); + QApplication::processEvents(); + } +} + +/// The value field a slider owns, so a typed commit can be exercised. +QLineEdit *field_of(QWidget *slider) +{ + return slider->findChild(); +} + +/// Press, drag and release at `x`, which is how a rail is actually driven. +void click_rail(QWidget *slider, int x) +{ + const QPoint pos(x, slider->height() / 2); + + for (auto type : {QEvent::MouseButtonPress, QEvent::MouseButtonRelease}) + { + QMouseEvent event(type, + QPointF(pos), + QPointF(slider->mapToGlobal(pos)), + Qt::LeftButton, + Qt::LeftButton, + Qt::NoModifier); + QApplication::sendEvent(slider, &event); + } + + flush(); +} + +meta::Attribute *make_attr(meta::AttributeContainer &container, + const std::string &name, + float value, + float min, + float max) +{ + auto *attr = container.add(name, value); + auto &m = attr->metadata(); + m.add(meta::keys::ui::widget_type, std::string("SliderFloat")); + m.add(meta::keys::ui::label, std::string(name)); + m.add(meta::keys::constraints::min, min); + m.add(meta::keys::constraints::max, max); + return attr; +} + +} // namespace + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + const Theme theme; + RowContext ctx; + ctx.theme = &theme; + + meta::AttributeContainer container; + + // --- a log rail must not behave like a linear one, even from zero -------- + // + // Driven by clicking rather than by inspecting the mapping, because the + // mapping is private and clicking is what a user does. Both rails span + // 0..100 at the same width, so the same press lands at the same fraction of + // each, and a log rail must return a far smaller value there. + { + auto *linear = make_attr(container, "linear", 1.f, 0.f, 100.f); + auto *logged = make_attr(container, "logged", 1.f, 0.f, 100.f); + logged->metadata().add(meta::keys::ui::log_scale, true); + + industrial::ParamSlider linear_slider(*linear, ctx); + industrial::ParamSlider log_slider(*logged, ctx); + linear_slider.resize(400, 36); + log_slider.resize(400, 36); + flush(); + + const int x = 240; // somewhere along the rail, the same on both + + click_rail(&linear_slider, x); + click_rail(&log_slider, x); + + const float linear_value = linear_slider.get(); + const float log_value = log_slider.get(); + + std::cout << "same press: linear=" << linear_value << " log=" << log_value + << '\n'; + + check(linear_value > 0.f, "the linear rail responded to a press"); + check(log_value < linear_value, + "a log rail returns a smaller value than a linear one at the same " + "point, i.e. log_scale is actually applied"); + } + + // --- log scale is declined only when the range has no span -------------- + { + auto *attr = make_attr(container, + "log_unbounded", + 1.f, + 0.f, + std::numeric_limits::max()); + attr->metadata().add(meta::keys::ui::log_scale, true); + + industrial::ParamSlider slider(*attr, ctx); + slider.resize(400, 36); + flush(); + + slider.set(42.f); + check(std::abs(slider.get() - 42.f) < 1e-3f, + "an unbounded log slider still holds the value it was given"); + } + + // --- ui.drag_max: the rail stops short, typing does not ------------------ + { + auto *attr = make_attr(container, "soft_max", 2.f, 0.f, 512.f); + attr->metadata().add(meta::keys::ui::drag_max, 64.f); + + industrial::ParamSlider slider(*attr, ctx); + slider.resize(400, 36); + flush(); + + QLineEdit *field = field_of(&slider); + check(field != nullptr, "the soft max slider has a value field"); + + if (field) + { + field->setText("512"); + Q_EMIT field->editingFinished(); + flush(); + + check(slider.get() > 64.f, + "a typed value above the rail maximum is accepted"); + check(std::abs(slider.get() - 512.f) < 1e-2f, + "a typed value is held to the real maximum, not to the rail"); + } + + // Dragging is still held to the rail, which is the whole point of the key. + // Pressed near the right hand end of the rail rather than past the widget, + // because a press outside the rail is correctly ignored and would prove + // nothing. + click_rail(&slider, slider.width() - 100); + check(slider.get() <= 64.f + 1e-3f, + "dragging stops at the rail maximum even though typing did not"); + } + + // --- no ui.drag_max: the real maximum is the rail maximum --------------- + // + // The case the old max == 64 rule broke. Islands is 1..64 and that 64 is a + // hard cap, so a typed 999 must come back clamped. + { + auto *attr = make_attr(container, "islands", 5.f, 1.f, 64.f); + + industrial::ParamSlider slider(*attr, ctx); + slider.resize(400, 36); + flush(); + + QLineEdit *field = field_of(&slider); + if (field) + { + field->setText("999"); + Q_EMIT field->editingFinished(); + flush(); + + check(slider.get() <= 64.f, + "a hard maximum of 64 still clamps a typed value"); + } + } + + std::cout << "slider mapping checks: failures=" << failures << '\n'; + return failures == 0 ? 0 : 1; +}