first commit
This commit is contained in:
387
modern-qt/widget/select.impl.hh
Normal file
387
modern-qt/widget/select.impl.hh
Normal file
@@ -0,0 +1,387 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_SELECT_IMPL_HH
|
||||
#define TOUCHSENSOR_SELECT_IMPL_HH
|
||||
|
||||
#include <qabstractitemview.h>
|
||||
|
||||
#include "select.hh"
|
||||
#include "modern-qt/utility/animation/animatable.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-qt/utility/painter/common.hh"
|
||||
#include "modern-qt/utility/painter/container.hh"
|
||||
#include "modern-qt/utility/painter/helper.hh"
|
||||
#include "modern-qt/utility/painter/shape.hh"
|
||||
|
||||
#include "qpainter.h"
|
||||
#include "qpainterpath.h"
|
||||
#include "select.hh"
|
||||
|
||||
using namespace creeper::select_widget::internal;
|
||||
|
||||
struct BasicSelect::Impl {
|
||||
public:
|
||||
explicit Impl(BasicSelect& self) noexcept
|
||||
: self{self}, animatable{self} {
|
||||
{
|
||||
auto state = std::make_shared<PidState<double>>();
|
||||
|
||||
state->config.kp = 20.0;
|
||||
state->config.ki = 00.0;
|
||||
state->config.kd = 00.0;
|
||||
state->config.epsilon = 1e-2;
|
||||
|
||||
label_position = make_transition(animatable, std::move(state));
|
||||
}
|
||||
|
||||
set_measurements(Measurements{});
|
||||
}
|
||||
|
||||
auto set_color_scheme(const ColorScheme& scheme) -> void {
|
||||
color_specs.enabled.container = scheme.surface_container_highest;
|
||||
color_specs.enabled.label_text = scheme.on_surface_variant;
|
||||
color_specs.enabled.selected_text = scheme.on_surface_variant;
|
||||
color_specs.enabled.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.enabled.active_indicator = scheme.on_surface_variant;
|
||||
color_specs.enabled.supporting_text = scheme.on_surface_variant;
|
||||
color_specs.enabled.input_text = scheme.on_surface;
|
||||
color_specs.enabled.caret = scheme.primary;
|
||||
color_specs.enabled.outline = scheme.outline;
|
||||
|
||||
color_specs.disabled.container = scheme.on_surface;
|
||||
color_specs.disabled.container.setAlphaF(0.04);
|
||||
color_specs.disabled.label_text = scheme.on_surface;
|
||||
color_specs.disabled.label_text.setAlphaF(0.38);
|
||||
color_specs.disabled.selected_text = scheme.on_surface;
|
||||
color_specs.disabled.selected_text.setAlphaF(0.04);
|
||||
color_specs.disabled.leading_icon = scheme.on_surface;
|
||||
color_specs.disabled.leading_icon.setAlphaF(0.38);
|
||||
color_specs.disabled.supporting_text = scheme.on_surface;
|
||||
color_specs.disabled.supporting_text.setAlphaF(0.38);
|
||||
color_specs.disabled.input_text = scheme.on_surface;
|
||||
color_specs.disabled.input_text.setAlphaF(0.38);
|
||||
color_specs.disabled.active_indicator = scheme.on_surface;
|
||||
color_specs.disabled.active_indicator.setAlphaF(0.38);
|
||||
color_specs.disabled.outline = scheme.outline;
|
||||
color_specs.disabled.outline.setAlphaF(0.38);
|
||||
|
||||
color_specs.focused.container = scheme.surface_container_highest;
|
||||
color_specs.focused.label_text = scheme.primary;
|
||||
color_specs.focused.selected_text = scheme.on_surface_variant;
|
||||
color_specs.focused.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.focused.input_text = scheme.on_surface;
|
||||
color_specs.focused.supporting_text = scheme.on_surface_variant;
|
||||
color_specs.focused.active_indicator = scheme.primary;
|
||||
color_specs.focused.outline = scheme.primary;
|
||||
|
||||
color_specs.error.container = scheme.surface_container_highest;
|
||||
color_specs.error.active_indicator = scheme.error;
|
||||
color_specs.error.label_text = scheme.error;
|
||||
color_specs.error.selected_text = scheme.on_surface;
|
||||
color_specs.error.input_text = scheme.on_surface;
|
||||
color_specs.error.supporting_text = scheme.error;
|
||||
color_specs.error.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.error.caret = scheme.error;
|
||||
color_specs.error.outline = scheme.error;
|
||||
|
||||
color_specs.state_layer = scheme.on_surface;
|
||||
color_specs.state_layer.setAlphaF(0.08);
|
||||
color_specs.selection_container = scheme.primary;
|
||||
color_specs.selection_container.setAlphaF(0.38);
|
||||
|
||||
const auto& color = get_color_tokens();
|
||||
sync_basic_text_style(
|
||||
color.input_text,
|
||||
scheme.surface_container_highest,
|
||||
color.input_text,
|
||||
color_specs.selection_container,
|
||||
color_specs.selection_container
|
||||
);
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& manager) -> void {
|
||||
manager.append_handler(&self, [this](const ThemeManager& manager) {
|
||||
set_color_scheme(manager.color_scheme());
|
||||
});
|
||||
}
|
||||
|
||||
auto set_label_text(const QString& text) -> void { label_text = text; }
|
||||
|
||||
auto set_leading_icon(const QString& code, const QString& font) -> void {
|
||||
leading_icon_code = code;
|
||||
leading_icon_font = font;
|
||||
is_update_component_status = false;
|
||||
}
|
||||
|
||||
auto set_measurements(const Measurements& measurements) -> void {
|
||||
this->measurements = measurements;
|
||||
self.setFixedHeight(measurements.container_height + measurements.standard_font_height);
|
||||
is_update_component_status = false;
|
||||
}
|
||||
|
||||
auto paint_filled(QPaintEvent*) -> void {
|
||||
const auto rect = self.rect();
|
||||
const auto color = get_color_tokens();
|
||||
|
||||
|
||||
constexpr auto container_radius = 5;
|
||||
update_component_status();
|
||||
|
||||
auto painter = QPainter{&self};
|
||||
|
||||
{
|
||||
util::PainterHelper{painter}
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color.container, Qt::transparent, 0,
|
||||
rect, container_radius, container_radius, 0, 0);
|
||||
}
|
||||
|
||||
{
|
||||
const auto p0 = rect.bottomLeft();
|
||||
const auto p1 = rect.bottomRight();
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen{color.active_indicator, filled_line_width()});
|
||||
painter.drawLine(p0, p1);
|
||||
}
|
||||
|
||||
if (!leading_icon.isNull()) {
|
||||
// TODO: 要不要画个默认的
|
||||
}
|
||||
else if (!leading_icon_code.isEmpty()){
|
||||
const auto rect = QRectF {
|
||||
1.0 * (self.width() - self.textMargins().right() - measurements.icon_rect_size),
|
||||
0.5 * (self.height() - measurements.icon_rect_size),
|
||||
1.0 * measurements.icon_rect_size,
|
||||
1.0 * measurements.icon_rect_size,
|
||||
};
|
||||
|
||||
painter.save();
|
||||
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen {color.leading_icon});
|
||||
painter.setFont(leading_icon_font);
|
||||
|
||||
const auto icon_center = rect.center();
|
||||
painter.translate(icon_center);
|
||||
const bool is_active = (self.view() && self.view()->isVisible());
|
||||
painter.rotate(is_active ? 180.0 : 0.0);
|
||||
painter.translate(-icon_center);
|
||||
painter.drawText(rect, leading_icon_code, {Qt::AlignCenter});
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
if (!label_text.isEmpty()) {
|
||||
const auto margins = self.textMargins();
|
||||
|
||||
const auto center_label_padding = 0.5 * (measurements.container_height);
|
||||
const auto rect_center = QRectF {
|
||||
QPointF {static_cast<double>(margins.left()), center_label_padding},
|
||||
QPointF (self.width() - margins.right(), self.height() - center_label_padding),
|
||||
};
|
||||
const auto rect_top = QRectF {
|
||||
QPointF (margins.left(), measurements.col_padding),
|
||||
QPointF (self.width() - margins.right(), margins.top()),
|
||||
};
|
||||
|
||||
const auto position = self.currentText().isEmpty() ? *label_position : 1.;
|
||||
const auto label_rect = animate::interpolate(rect_center, rect_top, position);
|
||||
const auto scale = 1. - position * 0.25;
|
||||
|
||||
painter.save();
|
||||
const auto label_anchor = QPointF {label_rect.left(), label_rect.center().y()};
|
||||
painter.translate(label_anchor);
|
||||
painter.scale(scale, scale);
|
||||
painter.translate(-label_anchor);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen {color.label_text});
|
||||
painter.setFont(standard_text_font);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.drawText(label_rect, label_text, {Qt::AlignVCenter | Qt::AlignLeading});
|
||||
painter.restore();
|
||||
|
||||
|
||||
if (self.currentIndex() != -1) {
|
||||
qDebug() << "paint currentText index: " << self.currentIndex();
|
||||
painter.save();
|
||||
const auto rect_center_selected = QRectF {
|
||||
QPointF {static_cast<double>(margins.left()), center_label_padding},
|
||||
QPointF (self.width() - margins.right(), self.height() - 0.5 * center_label_padding),
|
||||
};
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen {color.selected_text});
|
||||
painter.setFont(standard_text_font);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.drawText(rect_center_selected, self.currentText(), Qt::AlignVCenter | Qt::AlignLeading);
|
||||
painter.restore();
|
||||
}
|
||||
}
|
||||
else if (!label_text.isEmpty() && self.currentIndex() != -1) {
|
||||
const auto selected_text = self.currentText();
|
||||
const auto position = self.currentText().isEmpty() ? *label_position : 1.;
|
||||
}
|
||||
|
||||
if (is_hovered) {
|
||||
util::PainterHelper{painter}
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color_specs.state_layer, Qt::transparent, 0, rect,
|
||||
container_radius, container_radius, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
auto enter_event(qt::EnterEvent*) -> void {
|
||||
is_hovered = true;
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto leave_event(QEvent*) -> void {
|
||||
is_hovered = false;
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto focus_in(QFocusEvent*) -> void {
|
||||
is_focused = true;
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto focus_out(QFocusEvent*) -> void {
|
||||
is_focused = false;
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto show_popup() -> void {
|
||||
if (self.count() == 0) {
|
||||
return;
|
||||
}
|
||||
is_active = true;
|
||||
self.QComboBox::showPopup();
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto hide_popup() -> void {
|
||||
is_active = false;
|
||||
self.QComboBox::hidePopup();
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
auto update_component_status() -> void {
|
||||
if (is_update_component_status) return;
|
||||
|
||||
auto font = self.font();
|
||||
font.setPixelSize(measurements.standard_font_height);
|
||||
self.setFont(font);
|
||||
standard_text_font = self.font();
|
||||
standard_text_font.setPixelSize(measurements.standard_font_height);
|
||||
|
||||
is_update_component_status = true;
|
||||
}
|
||||
|
||||
auto update_label_position() -> void {
|
||||
if ((is_focused || is_active) && self.currentIndex() != -1) {
|
||||
label_position->transition_to(1.0);
|
||||
} else if (is_focused || is_active) {
|
||||
label_position->transition_to(1.0);
|
||||
} else {
|
||||
label_position->transition_to(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
auto sync_basic_text_style(const QColor& text, const QColor& background,
|
||||
const QColor& selection_text, const QColor& selection_background,
|
||||
const QColor& hover) -> void {
|
||||
constexpr auto to_rgba = [](const QColor& color) {
|
||||
return QStringLiteral("rgba(%1, %2, %3, %4)")
|
||||
.arg(color.red())
|
||||
.arg(color.green())
|
||||
.arg(color.blue())
|
||||
.arg(color.alpha());
|
||||
};
|
||||
|
||||
constexpr auto kQComboBoxStyle = R"(
|
||||
QComboBox QAbstractItemView {
|
||||
border: none;
|
||||
background-color: %1;
|
||||
outline: none;
|
||||
}
|
||||
QComboBox QAbstractItemView::item {
|
||||
padding: 3px 5px;
|
||||
color: %2;
|
||||
}
|
||||
QComboBox QAbstractItemView::item:selected {
|
||||
background-color: %3;
|
||||
color: %4;
|
||||
}
|
||||
QComboBox QAbstractItemView::item:hover {
|
||||
background-color: %5;
|
||||
color: %6;
|
||||
}
|
||||
)";
|
||||
|
||||
const auto qss = QString {kQComboBoxStyle};
|
||||
self.setStyleSheet(qss.arg(to_rgba(background))
|
||||
.arg(to_rgba(text))
|
||||
.arg(to_rgba(selection_background))
|
||||
.arg(to_rgba(selection_text))
|
||||
.arg(to_rgba(hover))
|
||||
.arg(to_rgba(text)));
|
||||
// self.setStyleSheet(qss.arg(to_rgba(text))
|
||||
// .arg(to_rgba(background))
|
||||
// .arg(to_rgba(selection_text))
|
||||
// .arg(to_rgba(selection_background)));
|
||||
}
|
||||
auto get_color_tokens() const -> ColorSpace::Tokens const& {
|
||||
return is_disable ? color_specs.disabled
|
||||
: is_error ? color_specs.error
|
||||
: is_active ? color_specs.focused
|
||||
: is_focused ? color_specs.focused
|
||||
: color_specs.enabled;
|
||||
}
|
||||
|
||||
auto filled_line_width() const -> double {
|
||||
constexpr auto normal_width = 1;
|
||||
constexpr auto active_width = 3;
|
||||
return (is_focused && !is_disable) ? active_width : normal_width;
|
||||
}
|
||||
|
||||
constexpr auto measure_text(const QFont& font, const QString& text, const QTextOption& options)
|
||||
-> double {
|
||||
const auto fm = QFontMetricsF(font);
|
||||
const auto size = fm.size(Qt::TextSingleLine, text);
|
||||
return size.width();
|
||||
}
|
||||
private:
|
||||
Measurements measurements;
|
||||
ColorSpace color_specs;
|
||||
bool is_disable = false;
|
||||
bool is_hovered = false;
|
||||
bool is_focused = false;
|
||||
bool is_error = false;
|
||||
bool is_update_component_status = false;
|
||||
bool is_active = false;
|
||||
|
||||
QString label_text;
|
||||
|
||||
QIcon leading_icon;
|
||||
QString leading_icon_code;
|
||||
QString leading_font_name;
|
||||
|
||||
QFont leading_icon_font;
|
||||
QFont standard_text_font;
|
||||
|
||||
Animatable animatable;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> label_position;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> rotation_angle;
|
||||
|
||||
BasicSelect& self;
|
||||
};
|
||||
|
||||
#endif //TOUCHSENSOR_SELECT_IMPL_HH
|
||||
Reference in New Issue
Block a user