Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions MetaUI/qt/include/meta_qt/designs/industrial/param_slider.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ class ParamSlider : public Control<float>
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
Expand Down
18 changes: 18 additions & 0 deletions MetaUI/qt/include/meta_qt/ui/number_format.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,26 @@
#include <QString>
#include <algorithm>
#include <cmath>
#include <string>
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)
{
Expand Down
29 changes: 27 additions & 2 deletions MetaUI/qt/src/designs/industrial/param_slider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <format>

#include "meta_qt/ui/number_format.hpp"

#include <algorithm>
Expand Down Expand Up @@ -49,7 +51,8 @@ ParamSlider::ParamSlider(Attribute<float> &attr,
log_scale_ = meta::common::try_get<bool>(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_);

Expand Down Expand Up @@ -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()
Expand Down
60 changes: 50 additions & 10 deletions tests/test_qt/test_slider_mapping/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 *>();
}
QLineEdit *field_of(QWidget *slider) { return slider->findChild<QLineEdit *>(); }

/// Press, drag and release at `x`, which is how a rail is actually driven.
void click_rail(QWidget *slider, int x)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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");
}
}

Expand Down