From cd806311fdeb30ac660c2ed3015f4544c3b8c06c Mon Sep 17 00:00:00 2001 From: Leonhardmaster2 Date: Mon, 7 Sep 2026 20:50:26 +0200 Subject: [PATCH] fix(qt): render the value readout in the format the attribute declared The industrial readout always rendered fixed, whatever the attribute asked for. HydraulicParticle declares its rates as "{:.2e}" over [1e-6, 1e-1] precisely so the readout stays a short mantissa and an exponent; rendered fixed, 1e-3 becomes 0.00100000, overruns the value field and clips to a run of zeroes with no decimal point visible. That is the 000001 in the reported screenshot. The whole spec is kept now rather than being reduced to a decimal count, and a spec whose presentation type is e, E, g or G is rendered through std::vformat. Everything else still goes through display_float, which widens precision rather than letting a small non-zero value round away to 0.00, so the fixed case keeps the behaviour it was given. A malformed spec falls back rather than throwing: that is a host bug and not a reason for a row to render nothing. --- .../designs/industrial/param_slider.hpp | 9 +++ .../qt/include/meta_qt/ui/number_format.hpp | 18 ++++++ .../src/designs/industrial/param_slider.cpp | 29 ++++++++- tests/test_qt/test_slider_mapping/main.cpp | 60 +++++++++++++++---- 4 files changed, 104 insertions(+), 12 deletions(-) diff --git a/MetaUI/qt/include/meta_qt/designs/industrial/param_slider.hpp b/MetaUI/qt/include/meta_qt/designs/industrial/param_slider.hpp index c47c291..991b247 100644 --- a/MetaUI/qt/include/meta_qt/designs/industrial/param_slider.hpp +++ b/MetaUI/qt/include/meta_qt/designs/industrial/param_slider.hpp @@ -98,6 +98,15 @@ class ParamSlider : public Control float value_ = 0.f; bool log_scale_ = false; int decimals_ = 2; + + /** @brief The attribute's declared format spec, e.g. "{:.2e}". + * + * Kept whole rather than reduced to a decimal count, because the + * presentation type carries as much intent as the precision does. A rate + * declared over five decades wants 1.00e-03, not 0.001, and certainly not + * the eight characters of 0.00000100 that a fixed readout produces. + */ + std::string format_; std::string label_; std::string category_; std::string key_; ///< attribute name, for the defaults lookup on reset diff --git a/MetaUI/qt/include/meta_qt/ui/number_format.hpp b/MetaUI/qt/include/meta_qt/ui/number_format.hpp index c99ac41..9ab337f 100644 --- a/MetaUI/qt/include/meta_qt/ui/number_format.hpp +++ b/MetaUI/qt/include/meta_qt/ui/number_format.hpp @@ -5,8 +5,26 @@ #include #include #include +#include namespace meta::qt { +/** @brief True when a std::format spec asks for scientific or general form. + * + * Looks only at the presentation type, the last character before the closing + * brace, so "{:.2e}" and "{:10.3E}" both match while "{:.2f}" does not. A + * spec that names one of these is asking for an exponent, and the readout has + * to give it one rather than substituting a fixed rendering of its own. + */ +inline bool has_exponent_format(const std::string &spec) +{ + const auto close = spec.find_last_of('}'); + if (close == std::string::npos || close == 0) + return false; + + const char type = spec[close - 1]; + return type == 'e' || type == 'E' || type == 'g' || type == 'G'; +} + // Ordinary values stay compact; small nonzero values must never read as zero. inline QString display_float(float value, int decimals = 2) { diff --git a/MetaUI/qt/src/designs/industrial/param_slider.cpp b/MetaUI/qt/src/designs/industrial/param_slider.cpp index adde5d4..f1429f4 100644 --- a/MetaUI/qt/src/designs/industrial/param_slider.cpp +++ b/MetaUI/qt/src/designs/industrial/param_slider.cpp @@ -2,6 +2,8 @@ Public License. The full license is in the file LICENSE, distributed with this software. */ #include "meta_qt/designs/industrial/param_slider.hpp" +#include + #include "meta_qt/ui/number_format.hpp" #include @@ -49,7 +51,8 @@ ParamSlider::ParamSlider(Attribute &attr, log_scale_ = meta::common::try_get(attr, meta::keys::ui::log_scale, false); - decimals_ = meta::common::try_get_format_decimals(meta::common::format(attr)); + format_ = meta::common::format(attr); + decimals_ = meta::common::try_get_format_decimals(format_); unbounded_ = !has_usable_range(min_, max_); @@ -526,7 +529,29 @@ void ParamSlider::commit_value(float value) QString ParamSlider::format_value(float value) const { - return display_float(value); + // Honour the presentation type the attribute declared. A rate spanning five + // decades declares "{:.2e}" precisely so its readout stays three characters + // of mantissa and an exponent; rendering it fixed gives 0.00000100, which + // does not fit the value field and reads as a truncated number rather than + // a small one. + // + // Only e and g are routed here. A fixed spec goes to display_float, which + // widens the precision rather than letting a small non-zero value round away + // to 0.00, and that is the behaviour a fixed readout wants. + if (has_exponent_format(format_)) + { + try + { + return QString::fromStdString( + std::vformat(format_, std::make_format_args(value))); + } + catch (const std::format_error &) + { + // A malformed spec is a host bug, not a reason to render nothing. + } + } + + return display_float(value, decimals_); } void ParamSlider::refresh_field() diff --git a/tests/test_qt/test_slider_mapping/main.cpp b/tests/test_qt/test_slider_mapping/main.cpp index 2a4357c..57886f0 100644 --- a/tests/test_qt/test_slider_mapping/main.cpp +++ b/tests/test_qt/test_slider_mapping/main.cpp @@ -49,10 +49,7 @@ void flush() } /// The value field a slider owns, so a typed commit can be exercised. -QLineEdit *field_of(QWidget *slider) -{ - return slider->findChild(); -} +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) @@ -125,8 +122,7 @@ int main(int argc, char **argv) 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'; + 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, @@ -170,8 +166,7 @@ int main(int argc, char **argv) Q_EMIT field->editingFinished(); flush(); - check(slider.get() > 64.f, - "a typed value above the rail maximum is accepted"); + 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"); } @@ -203,8 +198,53 @@ int main(int argc, char **argv) Q_EMIT field->editingFinished(); flush(); - check(slider.get() <= 64.f, - "a hard maximum of 64 still clamps a typed value"); + check(slider.get() <= 64.f, "a hard maximum of 64 still clamps a typed value"); + } + } + + // --- the readout honours the declared presentation type ----------------- + // + // HydraulicParticle declares its rates as "{:.2e}" over [1e-6, 1e-1]. Rendered + // fixed, 1e-3 comes out as 0.00100000 and overruns the value field; Otto saw + // it clipped to a run of zeroes with no decimal point in sight. + { + auto *attr = make_attr(container, "drag_rate", 1e-3f, 1e-6f, 1e-1f); + attr->metadata().add(meta::keys::ui::format, std::string("{:.2e}")); + attr->metadata().add(meta::keys::ui::log_scale, true); + + industrial::ParamSlider slider(*attr, ctx); + slider.resize(400, 36); + flush(); + + QLineEdit *field = field_of(&slider); + check(field != nullptr, "the rate slider has a value field"); + + if (field) + { + std::cout << "scientific readout: " << field->text().toStdString() << '\n'; + check(field->text().contains('e') || field->text().contains('E'), + "a {:.2e} attribute renders with an exponent"); + check(field->text().size() <= 10, + "the scientific readout is short enough for the value field"); + } + } + + // --- a fixed spec still widens rather than rounding a small value away --- + { + auto *attr = make_attr(container, "fixed_small", 0.001f, 0.f, 1.f); + attr->metadata().add(meta::keys::ui::format, std::string("{:.2f}")); + + industrial::ParamSlider slider(*attr, ctx); + slider.resize(400, 36); + flush(); + + QLineEdit *field = field_of(&slider); + if (field) + { + std::cout << "fixed readout: " << field->text().toStdString() << '\n'; + check(field->text() != "0.00", + "a small value under a fixed spec does not read as zero"); + check(!field->text().contains('e'), "a fixed spec is not promoted to scientific"); } }