Files
ts-qt/components/view.cc

821 lines
31 KiB
C++

//
// Created by Lenn on 2025/10/14.
//
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <exception>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
#include <QString>
#include <QObject>
#include <QMetaObject>
#include <QStringList>
#include <QtCore/Qt>
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QStandardPaths>
#include <QPair>
#include <optional>
#include <string>
#include <qsize.h>
#include <qsizepolicy.h>
#include <chrono>
#include <QEvent>
#include <iomanip>
#include <iostream>
#include <sstream>
#include "component.hh"
#include "cpstream_core.hh"
#include "base/globalhelper.hh"
#include "creeper-qt/utility/theme/theme.hh"
#include "creeper-qt/utility/wrapper/layout.hh"
#include "creeper-qt/utility/wrapper/widget.hh"
#include "components/charts/heatmap.hh"
#include <creeper-qt/layout/flow.hh>
#include <creeper-qt/layout/linear.hh>
#include <creeper-qt/utility/material-icon.hh>
#include <creeper-qt/utility/wrapper/mutable-value.hh>
#include <creeper-qt/widget/buttons/icon-button.hh>
#include <creeper-qt/widget/buttons/filled-button.hh>
#include <creeper-qt/widget/cards/filled-card.hh>
#include <creeper-qt/widget/cards/outlined-card.hh>
#include <creeper-qt/widget/image.hh>
#include <creeper-qt/widget/shape/wave-circle.hh>
#include <creeper-qt/widget/sliders.hh>
#include <creeper-qt/widget/switch.hh>
#include <creeper-qt/widget/text-fields.hh>
#include <creeper-qt/widget/text.hh>
#include <creeper-qt/widget/dropdown-menu.hh>
#include "components/ffmsep/presist/presist.hh"
#include "components/ffmsep/tactile/tacdec.hh"
#include <qstringliteral.h>
#define DEBUG 0
using namespace creeper;
namespace capro = card::pro;
namespace lnpro = linear::pro;
namespace impro = image::pro;
namespace ibpro = icon_button::pro;
namespace fbpro = filled_button::pro;
namespace dmpro = dropdown_menu::pro;
namespace pwpro = plot_widget::pro;
namespace {
constexpr std::array<std::uint8_t, 14> kSlaveRequestCommand{
0x55,
0xAA,
0x09,
0x00,
0x34,
0x00,
0xFB,
0x00,
0x1C,
0x00,
0x00,
0x18,
0x00,
0x7A
};
QVector<PointData> make_flat_points(const QSize& size, double value = 0.0) {
const int width = std::max(size.width(), 1);
const int height = std::max(size.height(), 1);
QVector<PointData> points;
points.reserve(static_cast<int>(width * height));
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
points.append(PointData{
static_cast<double>(x),
static_cast<double>(y),
value });
}
}
return points;
}
std::once_flag& codec_registration_flag() {
static std::once_flag flag;
return flag;
}
class SensorStreamController: public QObject {
public:
SensorStreamController(std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data,
std::shared_ptr<MutableValue<QSize>> matrix_context,
QObject* parent = nullptr): QObject(parent), heatmap_data_(std::move(heatmap_data)), matrix_context_(std::move(matrix_context)) {
std::call_once(codec_registration_flag(), [] {
ffmsep::tactile::register_tactile_codec();
});
}
~SensorStreamController() override {
reset_core();
}
bool start(const QString& requested_port, std::uint32_t baudrate) {
if (is_connected()) {
return true;
}
const auto ports = ffmsep::CPStreamCore::list_available_ports();
std::string port_utf8;
if (!requested_port.isEmpty()) {
port_utf8 = requested_port.toStdString();
const auto it = std::find_if(
ports.begin(), ports.end(), [&](const serial::PortInfo& info) { return info.port == port_utf8; });
if (it == ports.end()) {
if (ports.empty()) {
#if DEBUG
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available and no other ports detected.\n";
#endif
last_error_ = QString::fromUtf8("未检测到串口");
return false;
}
#if DEBUG
std::cerr << "SensorStreamController: requested port '" << port_utf8 << "' not available, falling back to first detected port.\n";
#endif
port_utf8 = ports.front().port;
}
}
else if (!ports.empty()) {
port_utf8 = ports.front().port;
}
else {
#if DEBUG
std::cerr << "SensorStreamController: no serial ports available\n";
#endif
last_error_ = QString::fromUtf8("未检测到串口");
return false;
}
const std::uint32_t baud = baudrate == 0U ? 115200U : baudrate;
ffmsep::CPStreamConfig cfg;
cfg.port = port_utf8;
cfg.baudrate = baud;
cfg.codec_id = ffmsep::CPCodecID::Tactile;
cfg.read_chunk_size = 256;
cfg.packet_queue_capacity = 128;
cfg.frame_queue_capacity = 32;
cfg.slave_request_command.assign(kSlaveRequestCommand.begin(), kSlaveRequestCommand.end());
cfg.slave_request_interval = 3ms;
reset_core();
core_ = std::make_unique<ffmsep::CPStreamCore>();
if (!core_->open(cfg)) {
last_error_ = QString::fromStdString(core_->last_error());
std::cerr << "SensorStreamController: open failed - " << core_->last_error() << "\n";
reset_core();
return false;
}
core_->set_frame_callback([this](std::shared_ptr<ffmsep::DecodedFrame> frame) {
handle_frame(frame);
});
if (!core_->start()) {
last_error_ = QString::fromStdString(core_->last_error());
std::cerr << "SensorStreamController: start failed - " << core_->last_error() << "\n";
reset_core();
return false;
}
active_port_ = QString::fromStdString(cfg.port);
last_error_.clear();
connected_ = true;
return true;
}
void stop() {
if (!core_) {
active_port_.clear();
if (heatmap_data_ && matrix_context_) {
heatmap_data_->set(make_flat_points(matrix_context_->get()));
}
connected_ = false;
return;
}
core_->set_frame_callback({});
if (core_->is_running()) {
core_->stop();
}
core_->clear_frames();
connected_ = false;
active_port_.clear();
if (heatmap_data_ && matrix_context_) {
heatmap_data_->set(make_flat_points(matrix_context_->get()));
}
}
[[nodiscard]] bool is_running() const noexcept {
return core_ && core_->is_running();
}
[[nodiscard]] bool is_connected() const noexcept {
return connected_;
}
[[nodiscard]] QString active_port() const {
return active_port_;
}
[[nodiscard]] QString last_error() const {
return last_error_;
}
std::future<ffmsep::persist::WriteResult> export_frames(const QString& path,
bool clear_after_export) {
if (path.isEmpty()) {
return make_failed_future(path, "export path is empty");
}
if (!core_) {
return make_failed_future(path, "stream is not active");
}
if (core_->recorded_frame_count() == 0U) {
return make_failed_future(path, "no tactile frames recorded");
}
const auto normalized = QDir::toNativeSeparators(path);
return core_->export_recorded_frames(normalized.toStdString(), clear_after_export);
}
private:
void reset_core() {
connected_ = false;
if (!core_) {
return;
}
core_->set_frame_callback({});
if (core_->is_running()) {
core_->stop();
}
if (core_->is_open()) {
core_->close();
}
core_.reset();
}
static QSize to_qsize(const ffmsep::tactile::MatrixSize& m) {
return QSize{
static_cast<int>(m.long_edge),
static_cast<int>(m.short_edge)
};
}
void handle_frame(std::shared_ptr<ffmsep::DecodedFrame> frame) {
if (!frame->tactile || frame->tactile_pressures.empty()) {
return;
}
auto pressures = frame->tactile_pressures;
auto frame_bytes = frame->frame.data;
std::vector<std::uint8_t> raw_payload;
if (frame->tactile) {
raw_payload = frame->tactile->payload;
}
QMetaObject::invokeMethod(
this,
[this,
pressures = std::move(pressures),
frame_bytes = std::move(frame_bytes),
raw_payload = std::move(raw_payload)]() mutable {
const auto format_raw = [](const std::vector<std::uint8_t>& data) -> std::string {
if (data.empty()) {
return "[]";
}
std::ostringstream oss;
oss << '[';
oss << std::uppercase << std::setfill('0');
for (std::size_t idx = 0; idx < data.size(); ++idx) {
if (idx != 0U) {
oss << ' ';
}
oss << std::setw(2) << std::hex << static_cast<unsigned int>(data[idx]);
}
oss << ']';
return oss.str();
};
std::cout << "[Sensor] frame=" << format_raw(frame_bytes);
std::cout << " payload=" << format_raw(raw_payload);
std::cout << " received " << pressures.size() << " pressure values";
const std::size_t preview = std::min<std::size_t>(pressures.size(), 12);
if (preview > 0) {
std::cout << " values=[";
for (std::size_t idx = 0; idx < preview; ++idx) {
if (idx != 0U) {
std::cout << ", ";
}
std::cout << pressures[idx];
}
if (preview < pressures.size()) {
std::cout << ", ...";
}
std::cout << "]";
}
std::cout << std::endl;
auto matrix = matrix_context_->get();
const auto cells_exp = static_cast<std::size_t>(std::max(1, matrix.width()) *
std::max(1, matrix.height()));
if (cells_exp == 0) return;
if (pressures.size() > cells_exp) {
pressures.resize(cells_exp);
} else if (pressures.size() < cells_exp) {
pressures.resize(cells_exp, 0);
}
QVector<PointData> points;
points.reserve(matrix.width() * matrix.height());
for (int y = 0; y < matrix.height(); ++y) {
for (int x = 0; x < matrix.width(); ++x) {
const int idx = y * matrix.width() + x;
if (idx >= static_cast<int>(pressures.size())) {
break;
}
const auto value = static_cast<double>(pressures[static_cast<std::size_t>(idx)]);
points.append(PointData{
static_cast<double>(x),
static_cast<double>(y),
value });
}
}
matrix_context_->set(matrix);
heatmap_data_->set(std::move(points));
},
Qt::QueuedConnection);
}
[[nodiscard]] QSize normalize_matrix(QSize candidate, std::size_t value_count) const {
if (value_count == 0U) {
return QSize{};
}
const auto adapt_from = [value_count](const QSize& hint) -> std::optional<QSize> {
if (hint.width() <= 0 && hint.height() <= 0) {
return std::nullopt;
}
if (hint.width() > 0 && hint.height() > 0) {
const auto cells = static_cast<std::size_t>(hint.width()) * static_cast<std::size_t>(hint.height());
if (cells == value_count) {
return hint;
}
}
if (hint.width() > 0) {
const auto width = static_cast<std::size_t>(hint.width());
if (width != 0U && (value_count % width) == 0U) {
const auto height = static_cast<int>(value_count / width);
return QSize{ hint.width(), height };
}
}
if (hint.height() > 0) {
const auto height = static_cast<std::size_t>(hint.height());
if (height != 0U && (value_count % height) == 0U) {
const auto width = static_cast<int>(value_count / height);
return QSize{ width, hint.height() };
}
}
return std::nullopt;
};
if (auto adjusted = adapt_from(candidate)) {
return *adjusted;
}
if (auto adjusted = adapt_from(matrix_context_->get())) {
return *adjusted;
}
const auto root = static_cast<int>(std::sqrt(static_cast<double>(value_count)));
for (int width = root; width >= 1; --width) {
const auto divisor = static_cast<std::size_t>(width);
if (divisor == 0U) {
continue;
}
if ((value_count % divisor) == 0U) {
const auto height = static_cast<int>(value_count / divisor);
return QSize{ width, height };
}
}
return QSize{ static_cast<int>(value_count), 1 };
}
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data_;
std::shared_ptr<MutableValue<QSize>> matrix_context_;
std::unique_ptr<ffmsep::CPStreamCore> core_;
QString active_port_;
QString last_error_;
bool connected_ = false;
static std::future<ffmsep::persist::WriteResult> make_failed_future(
const QString& path,
std::string message) {
std::promise<ffmsep::persist::WriteResult> promise;
auto future = promise.get_future();
ffmsep::persist::WriteResult result{
false,
std::move(message),
path.toStdString()
};
promise.set_value(std::move(result));
return future;
}
};
struct SensorUiState {
std::shared_ptr<MutableValue<QString>> link_icon =
std::make_shared<MutableValue<QString>>(QString::fromLatin1(material::icon::kAddLink));
std::shared_ptr<MutableValue<QVector<PointData>>> heatmap_data =
std::make_shared<MutableValue<QVector<PointData>>>();
std::shared_ptr<MutableValue<QSize>> heatmap_matrix =
std::make_shared<MutableValue<QSize>>();
std::shared_ptr<MutableValue<QPair<int, int>>> heatmap_range =
std::make_shared<MutableValue<QPair<int, int>>>(QPair<int, int>{0, 300});
std::shared_ptr<MutableValue<QStringList>> port_items =
std::make_shared<MutableValue<QStringList>>();
std::shared_ptr<MutableValue<QStringList>> profile_items =
std::make_shared<MutableValue<QStringList>>();
QString selected_port;
QString selected_profile;
std::uint32_t selected_baud = 115200;
std::unique_ptr<SensorStreamController> controller;
SensorUiState() {
const QSize size{ 3, 4 };
heatmap_matrix->set_silent(size);
heatmap_data->set_silent(make_flat_points(size));
// 初始化串口列表
QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info: ports) {
ports_list.emplace_back(QString::fromStdString(info.port));
}
port_items->set_silent(ports_list);
if (selected_port.isEmpty() && !ports_list.isEmpty()) {
selected_port = ports_list.front();
}
GlobalHelper::instance().reload_profiles();
QStringList profile_list;
const auto& profiles = GlobalHelper::instance().get_all_profile();
profile_list.reserve(static_cast<qsizetype>(profiles.size()));
for (const auto& p : profiles) {
profile_list << p.name;
}
profile_items->set_silent(profile_list);
if (selected_profile.isEmpty() && !profile_list.isEmpty()) {
selected_profile = profile_list.front();
if (!profiles.empty()) {
const auto size = QSize { std::max(1, profiles.front().matrix_width),
std::max(1, profiles.front().matrix_height) };
selected_baud = profiles.front().baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(profiles.front().baud_rate);
heatmap_matrix->set_silent(size);
heatmap_data->set_silent(make_flat_points(size));
const int range_min = profiles.front().range_left;
const int range_max = (profiles.front().range_right == profiles.front().range_left)
? profiles.front().range_left + 1
: profiles.front().range_right;
heatmap_range->set_silent(QPair<int, int>{range_min, range_max});
}
}
controller = std::make_unique<SensorStreamController>(heatmap_data, heatmap_matrix);
}
};
SensorUiState& sensor_state() {
static SensorUiState state;
return state;
}
static void RefreshPortsForView(SensorUiState& sensor) {
QStringList ports_list;
const auto ports = ffmsep::CPStreamCore::list_available_ports();
ports_list.reserve(static_cast<qsizetype>(ports.size()));
for (const auto& info: ports) {
ports_list.emplace_back(QString::fromStdString(info.port));
}
if (!sensor.selected_port.isEmpty()) {
const bool exists = ports_list.contains(sensor.selected_port);
if (!exists) {
sensor.selected_port = ports_list.isEmpty() ? QString{} : ports_list.front();
}
}
else if (!ports_list.isEmpty()) {
sensor.selected_port = ports_list.front();
}
sensor.port_items->set(std::move(ports_list));
RefreshProfilesForView();
}
class PortHoverRefreshFilter final : public QObject {
public:
explicit PortHoverRefreshFilter(SensorUiState& sensor, QObject* parent = nullptr)
: QObject(parent)
, sensor_(sensor) { }
protected:
bool eventFilter(QObject* watched, QEvent* event) override {
if (event && event->type() == QEvent::Enter) {
RefreshPortsForView(sensor_);
}
return QObject::eventFilter(watched, event);
}
private:
SensorUiState& sensor_;
};
} // namespace
void RefreshProfilesForView() {
auto& sensor = sensor_state();
GlobalHelper::instance().reload_profiles();
QStringList profile_list;
const auto& profiles = GlobalHelper::instance().get_all_profile();
profile_list.reserve(static_cast<qsizetype>(profiles.size()));
for (const auto& p : profiles) {
profile_list << p.name;
}
if (!sensor.selected_profile.isEmpty()) {
const bool exists = profile_list.contains(sensor.selected_profile);
if (!exists) {
sensor.selected_profile = profile_list.isEmpty() ? QString{} : profile_list.front();
}
}
else if (!profile_list.isEmpty()) {
sensor.selected_profile = profile_list.front();
}
if (!sensor.selected_profile.isEmpty()) {
const auto it = std::find_if(profiles.begin(), profiles.end(),
[&](const ConfigProfile& p) { return p.name == sensor.selected_profile; });
if (it != profiles.end()) {
sensor.selected_baud = it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
const auto size = QSize { std::max(1, it->matrix_width), std::max(1, it->matrix_height) };
sensor.heatmap_matrix->set(size);
sensor.heatmap_data->set(make_flat_points(size));
const int range_min = it->range_left;
const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right;
sensor.heatmap_range->set(QPair<int, int>{range_min, range_max});
}
}
sensor.profile_items->set(std::move(profile_list));
}
static auto ComConfigComponent(ThemeManager& manager) {
auto& sensor = sensor_state();
auto link_icon_context = sensor.link_icon;
if (sensor.selected_port.isEmpty() && !sensor.port_items->get().isEmpty()) {
sensor.selected_port = sensor.port_items->get().front();
}
if (sensor.selected_baud == 0U) {
sensor.selected_baud = 115200U;
}
const auto row = new Row{
// lnpro::Item<FilledTextField> {
// text_field::pro::ThemeManager {manager},
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
// MutableForward {
// text_field::pro::LabelText {},
// slogen_context,
// },
// },
lnpro::Item<FilledDropdownMenu>{
dmpro::ThemeManager{ manager },
dmpro::LeadingIcon{ material::icon::kArrowDropDown, material::regular::font },
dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
// const auto text = self.currentText();
if (!text.isEmpty()) {
sensor_ptr->selected_port = text;
}
} },
dmpro::LabelText{ "COM" },
MutableForward{
dmpro::Items{},
sensor.port_items,
},
dmpro::Apply{ [&sensor](dropdown_menu::internal::DropdownMenu& self) {
if (!self.property("portHoverRefreshAttached").toBool()) {
self.installEventFilter(new PortHoverRefreshFilter(sensor, &self));
self.setProperty("portHoverRefreshAttached", true);
}
} },
},
lnpro::Item<FilledDropdownMenu>{
dmpro::ThemeManager{ manager },
dmpro::LeadingIcon{ material::icon::kArrowDropDown, material::regular::font },
dmpro::TextChanged{ [sensor_ptr = &sensor](QString text) {
if (!text.isEmpty()) {
sensor_ptr->selected_profile = text;
const auto& profiles = GlobalHelper::instance().get_all_profile();
const auto it = std::find_if(profiles.begin(), profiles.end(),
[&text](const ConfigProfile& p) { return p.name == text; });
if (it != profiles.end()) {
const auto baud = it->baud_rate == 0 ? 115200U : static_cast<std::uint32_t>(it->baud_rate);
sensor_ptr->selected_baud = baud;
const auto size = QSize { std::max(1, it->matrix_width), std::max(1, it->matrix_height) };
sensor_ptr->heatmap_matrix->set(size);
sensor_ptr->heatmap_data->set(make_flat_points(size));
const int range_min = it->range_left;
const int range_max = (it->range_right == it->range_left) ? it->range_left + 1 : it->range_right;
sensor_ptr->heatmap_range->set(QPair<int, int>{range_min, range_max});
}
}
} },
dmpro::LabelText{ "Profile" },
MutableForward{
dmpro::Items{},
sensor.profile_items,
},
},
lnpro::SpacingItem{ 20 },
lnpro::Item<IconButton>{
ibpro::ThemeManager{ manager },
ibpro::FixedSize{ 40, 40 },
ibpro::Color{ IconButton::Color::TONAL },
ibpro::Font{ material::kRegularExtraSmallFont },
MutableForward{
icon_button::pro::FontIcon{},
link_icon_context,
},
ibpro::Clickable{ [sensor_ptr = &sensor, link_icon_context] {
auto& sensor = *sensor_ptr;
if (!sensor.controller) {
return;
}
if (sensor.controller->is_connected()) {
sensor.controller->stop();
link_icon_context->set(QString::fromLatin1(material::icon::kAddLink));
}
else {
const auto port = sensor.selected_port;
const auto baud = sensor.selected_baud == 0U ? 115200U : sensor.selected_baud;
if (sensor.controller->start(port, baud)) {
sensor.selected_port = sensor.controller->active_port();
link_icon_context->set(QString::fromLatin1(material::icon::kLinkOff));
}
else {
std::cerr << "Failed to start sensor stream: "
<< sensor.controller->last_error().toStdString()
<< "\n";
}
}
} } },
lnpro::Item<FilledButton>{
fbpro::ThemeManager{ manager },
fbpro::FixedSize{ 40, 40 },
fbpro::Radius{ 8.0 },
// fbpro::Color { IconButton::Color::TONAL },
fbpro::Font{ material::kRegularExtraSmallFont },
fbpro::Text{ "drive_file_move" },
fbpro::Clickable{ [&sensor] {
auto* controller = sensor.controller.get();
if (!controller) {
QMessageBox::warning(nullptr,
QStringLiteral("导出失败"),
QStringLiteral("当前串流尚未初始化。"));
return;
}
const auto documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
const auto timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss"));
QString suggested_name = QStringLiteral("touchsensor_%1.json").arg(timestamp);
QString initial_path = documents.isEmpty() ? suggested_name : QDir(documents).filePath(suggested_name);
const QString chosen_path = QFileDialog::getSaveFileName(
nullptr,
QStringLiteral("导出触觉帧"),
initial_path,
QStringLiteral("JSON 文件 (*.json)"));
if (chosen_path.isEmpty()) {
return;
}
auto future = controller->export_frames(chosen_path, false);
std::thread([future = std::move(future)]() mutable {
ffmsep::persist::WriteResult result{};
try {
result = future.get();
}
catch (const std::exception& ex) {
result.ok = false;
result.error = ex.what();
}
catch (...) {
result.ok = false;
result.error = "unknown export failure";
}
if (auto* app = QCoreApplication::instance()) {
QMetaObject::invokeMethod(app, [res = std::move(result)]() {
if (res.ok) {
QMessageBox::information(
nullptr,
QStringLiteral("导出成功"),
QStringLiteral("触觉帧已导出至:\n%1")
.arg(QString::fromStdString(res.path)));
} else {
const auto error = QString::fromStdString(
res.error.empty() ? std::string{"unknown error"} : res.error);
const auto target = QString::fromStdString(res.path);
const auto message = target.isEmpty()
? QStringLiteral("原因:%1").arg(error)
: QStringLiteral("原因:%1\n目标:%2").arg(error, target);
QMessageBox::warning(nullptr,
QStringLiteral("导出失败"),
message);
} }, Qt::QueuedConnection);
}
})
.detach();
} } }
};
return new Widget{
widget::pro::Layout{ row },
};
}
static auto DisplayComponent(ThemeManager& manager, int /*index*/ = 0) noexcept {
auto& sensor = sensor_state();
const auto row = new Row{
lnpro::Item<HeatMapPlot>{
plot_widget::pro::SizePolicy{
QSizePolicy::Expanding,
},
plot_widget::pro::ThemeManager{ manager },
MutableForward{
plot_widget::pro::PlotData{},
sensor.heatmap_data,
},
pwpro::MatrixSize{
sensor.heatmap_matrix->get() },
MutableTransform{
[](auto& widget, const QSize& size) {
pwpro::MatrixSize{ size }.apply(widget);
},
sensor.heatmap_matrix },
MutableTransform{
[](auto& widget, const QPair<int, int>& range) {
const double min = static_cast<double>(range.first);
const double max = static_cast<double>(range.second);
widget.set_color_gradient_range(min, max);
},
sensor.heatmap_range },
},
};
return new Widget{
widget::pro::Layout{ row },
};
}
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
return new FilledCard{
capro::ThemeManager{ state.manager },
capro::SizePolicy{ QSizePolicy::Expanding },
capro::Layout<Col>{
lnpro::Alignment{ Qt::AlignTop },
// lnpro::Margin {10},
// lnpro::Spacing {10},
lnpro::Item{
ComConfigComponent(state.manager),
},
lnpro::Item<Row>{
lnpro::Item{
DisplayComponent(state.manager),
},
lnpro::Item{
DisplayComponent(state.manager),
},
},
},
};
}