Merge branch 'dev'
# Conflicts: # components/ffmsep/cpstream_core.hh # components/view.cc
This commit is contained in:
26
creeper-qt/widget/buttons/button.hh
Normal file
26
creeper-qt/widget/buttons/button.hh
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
|
||||
namespace creeper::button::pro {
|
||||
|
||||
struct Button { };
|
||||
using Token = common::Token<Button>;
|
||||
|
||||
template <class Button>
|
||||
concept trait = std::derived_from<Button, Token>;
|
||||
|
||||
using Text = common::pro::Text<Token>;
|
||||
using TextColor = common::pro::TextColor<Token>;
|
||||
using Radius = common::pro::Radius<Token>;
|
||||
using BorderWidth = common::pro::BorderWidth<Token>;
|
||||
using BorderColor = common::pro::BorderColor<Token>;
|
||||
using Background = common::pro::Background<Token>;
|
||||
using WaterColor = common::pro::WaterColor<Token>;
|
||||
|
||||
template <typename Callback>
|
||||
using Clickable = common::pro::Clickable<Callback, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait)
|
||||
}
|
||||
172
creeper-qt/widget/buttons/filled-button.cc
Normal file
172
creeper-qt/widget/buttons/filled-button.cc
Normal file
@@ -0,0 +1,172 @@
|
||||
#include "filled-button.hh"
|
||||
|
||||
#include "creeper-qt/utility/animation/math.hh"
|
||||
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||
#include "creeper-qt/utility/animation/transition.hh"
|
||||
#include "creeper-qt/utility/animation/water-ripple.hh"
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
|
||||
#include <qevent.h>
|
||||
|
||||
namespace creeper::filled_button::internal {
|
||||
|
||||
constexpr auto kWaterSpeed = double { 5.0 };
|
||||
|
||||
struct FilledButton::Impl {
|
||||
public:
|
||||
bool enable_water_ripple = true;
|
||||
double water_ripple_step = 5.;
|
||||
|
||||
double radius = -1;
|
||||
QColor text_color = Qt::black;
|
||||
QColor background = Qt::white;
|
||||
|
||||
QColor border_color = Qt::red;
|
||||
double border_width = 0;
|
||||
|
||||
QColor water_color = Qt::black;
|
||||
|
||||
QColor kHoverColor = QColor { 0, 0, 0, 30 };
|
||||
bool is_mouse_hover = false;
|
||||
|
||||
Animatable animatable;
|
||||
WaterRippleRenderer water_ripple;
|
||||
|
||||
std::unique_ptr<TransitionValue<PidState<Eigen::Vector4d>>> hover_color;
|
||||
|
||||
public:
|
||||
explicit Impl(QAbstractButton& self)
|
||||
: animatable { self }
|
||||
, water_ripple { animatable, kWaterSpeed } {
|
||||
{
|
||||
auto state = std::make_shared<PidState<Eigen::Vector4d>>();
|
||||
|
||||
state->config.kp = 20;
|
||||
state->config.ki = 0;
|
||||
state->config.kd = 0;
|
||||
|
||||
hover_color = make_transition(animatable, std::move(state));
|
||||
}
|
||||
}
|
||||
|
||||
void paint_event(QAbstractButton& self, QPaintEvent* event) {
|
||||
|
||||
const auto radius = this->radius < 0
|
||||
? std::min<double>(self.rect().height(), self.rect().width()) / 2
|
||||
: this->radius;
|
||||
|
||||
const auto button_path = make_rounded_rectangle_path(self.rect(), radius);
|
||||
const auto hover_color = from_vector4(*this->hover_color);
|
||||
|
||||
auto painter = QPainter { &self };
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::RenderHint::Antialiasing)
|
||||
.set_opacity(1.)
|
||||
.rounded_rectangle(background, border_color, border_width, self.rect(), radius, radius)
|
||||
.apply(water_ripple.renderer(button_path, water_color))
|
||||
.set_opacity(1.)
|
||||
.rounded_rectangle(hover_color, Qt::transparent, 0, self.rect(), radius, radius)
|
||||
.set_opacity(1.)
|
||||
.simple_text(self.text(), self.font(), text_color, self.rect(), Qt::AlignCenter)
|
||||
.done();
|
||||
}
|
||||
|
||||
void mouse_release_event(QAbstractButton& self, QMouseEvent* event) {
|
||||
if (enable_water_ripple) {
|
||||
const auto center_point = event->pos();
|
||||
const auto max_distance = std::max<double>(self.width(), self.height());
|
||||
water_ripple.clicked(center_point, max_distance);
|
||||
}
|
||||
}
|
||||
|
||||
void enter_event(QAbstractButton& self, qt::EnterEvent* event) {
|
||||
hover_color->transition_to(from_color(kHoverColor));
|
||||
is_mouse_hover = true;
|
||||
}
|
||||
|
||||
void leave_event(QAbstractButton& self, QEvent* event) {
|
||||
hover_color->transition_to(from_color(Qt::transparent));
|
||||
is_mouse_hover = false;
|
||||
}
|
||||
|
||||
private:
|
||||
static QPainterPath make_rounded_rectangle_path(const QRectF& rect, double radius) {
|
||||
auto path = QPainterPath {};
|
||||
path.addRoundedRect(rect, radius, radius);
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
FilledButton::FilledButton()
|
||||
: pimpl(std::make_unique<Impl>(*this)) { }
|
||||
|
||||
FilledButton::~FilledButton() = default;
|
||||
|
||||
void FilledButton::set_color_scheme(const ColorScheme& color_scheme) {
|
||||
pimpl->background = color_scheme.primary;
|
||||
pimpl->text_color = color_scheme.on_primary;
|
||||
|
||||
if (color_scheme.primary.lightness() > 128) {
|
||||
pimpl->water_color = color_scheme.primary.darker(130);
|
||||
pimpl->kHoverColor = QColor { 000, 000, 000, 30 };
|
||||
} else {
|
||||
pimpl->water_color = color_scheme.primary.lighter(130);
|
||||
pimpl->kHoverColor = QColor { 255, 255, 255, 30 };
|
||||
}
|
||||
pimpl->water_color.setAlphaF(0.4);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void FilledButton::load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(this, [this](const ThemeManager& manager) {
|
||||
const auto color_mode = manager.color_mode();
|
||||
const auto theme_pack = manager.theme_pack();
|
||||
const auto color_scheme = color_mode == ColorMode::LIGHT //
|
||||
? theme_pack.light
|
||||
: theme_pack.dark;
|
||||
set_color_scheme(color_scheme);
|
||||
});
|
||||
}
|
||||
|
||||
// 属性设置接口实现
|
||||
|
||||
void FilledButton::set_radius(double radius) { pimpl->radius = radius; }
|
||||
|
||||
void FilledButton::set_border_width(double border) { pimpl->border_width = border; }
|
||||
|
||||
void FilledButton::set_border_color(const QColor& color) { pimpl->border_color = color; }
|
||||
|
||||
void FilledButton::set_water_color(const QColor& color) { pimpl->water_color = color; }
|
||||
|
||||
void FilledButton::set_text_color(const QColor& color) { pimpl->text_color = color; }
|
||||
|
||||
void FilledButton::set_background(const QColor& color) { pimpl->background = color; }
|
||||
|
||||
void FilledButton::set_hover_color(const QColor& color) { pimpl->kHoverColor = color; }
|
||||
|
||||
void FilledButton::set_water_ripple_status(bool enable) { pimpl->enable_water_ripple = enable; }
|
||||
|
||||
void FilledButton::set_water_ripple_step(double step) { pimpl->water_ripple_step = step; }
|
||||
|
||||
// Qt 接口重载
|
||||
|
||||
void FilledButton::mouseReleaseEvent(QMouseEvent* event) {
|
||||
pimpl->mouse_release_event(*this, event);
|
||||
QAbstractButton::mouseReleaseEvent(event);
|
||||
}
|
||||
void FilledButton::paintEvent(QPaintEvent* event) {
|
||||
pimpl->paint_event(*this, event);
|
||||
/* Disable QAbstractButton::paintEvent */;
|
||||
}
|
||||
void FilledButton::enterEvent(qt::EnterEvent* event) {
|
||||
pimpl->enter_event(*this, event);
|
||||
QAbstractButton::enterEvent(event);
|
||||
}
|
||||
void FilledButton::leaveEvent(QEvent* event) {
|
||||
pimpl->leave_event(*this, event);
|
||||
QAbstractButton::leaveEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
54
creeper-qt/widget/buttons/filled-button.hh
Normal file
54
creeper-qt/widget/buttons/filled-button.hh
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
#include "creeper-qt/widget/buttons/button.hh"
|
||||
#include "qabstractbutton.h"
|
||||
|
||||
namespace creeper::filled_button::internal {
|
||||
|
||||
class FilledButton : public QAbstractButton {
|
||||
CREEPER_PIMPL_DEFINITION(FilledButton);
|
||||
|
||||
public:
|
||||
void set_color_scheme(const ColorScheme& pack);
|
||||
void load_theme_manager(ThemeManager& manager);
|
||||
|
||||
void set_radius(double radius);
|
||||
void set_border_width(double border);
|
||||
|
||||
void set_water_color(const QColor& color);
|
||||
void set_border_color(const QColor& color);
|
||||
void set_text_color(const QColor& color);
|
||||
void set_background(const QColor& color);
|
||||
void set_hover_color(const QColor& color);
|
||||
|
||||
void set_water_ripple_status(bool enable);
|
||||
void set_water_ripple_step(double step);
|
||||
|
||||
protected:
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
|
||||
void enterEvent(qt::EnterEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::filled_button::pro {
|
||||
|
||||
using namespace button::pro;
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using FilledButton = Declarative<filled_button::internal::FilledButton,
|
||||
CheckerOr<button::pro::checker, widget::pro::checker, theme::pro::checker>>;
|
||||
|
||||
}
|
||||
41
creeper-qt/widget/buttons/filled-tonal-button.hh
Normal file
41
creeper-qt/widget/buttons/filled-tonal-button.hh
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include "filled-button.hh"
|
||||
|
||||
namespace creeper::filled_tonal_button::internal {
|
||||
class FilledTonalButton : public FilledButton {
|
||||
public:
|
||||
void set_color_scheme(const ColorScheme& color_scheme) {
|
||||
set_background(color_scheme.secondary_container);
|
||||
set_text_color(color_scheme.on_secondary_container);
|
||||
|
||||
auto water_color = QColor {};
|
||||
if (color_scheme.primary.lightness() > 128) {
|
||||
water_color = color_scheme.primary.darker(130);
|
||||
set_hover_color(QColor { 0, 0, 0, 30 });
|
||||
} else {
|
||||
water_color = color_scheme.primary.lighter(130);
|
||||
set_hover_color(QColor { 255, 255, 255, 30 });
|
||||
}
|
||||
water_color.setAlphaF(0.25);
|
||||
set_water_color(water_color);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
namespace creeper {
|
||||
|
||||
namespace filled_tonal_button::pro {
|
||||
using namespace filled_button::pro;
|
||||
}
|
||||
|
||||
using FilledTonalButton =
|
||||
Declarative<filled_tonal_button::internal::FilledTonalButton, FilledButton::Checker>;
|
||||
|
||||
}
|
||||
44
creeper-qt/widget/buttons/icon-button.cc
Normal file
44
creeper-qt/widget/buttons/icon-button.cc
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "icon-button.impl.hh"
|
||||
|
||||
IconButton::IconButton()
|
||||
: pimpl(std::make_unique<Impl>(*this)) { }
|
||||
|
||||
IconButton::~IconButton() = default;
|
||||
|
||||
void IconButton::set_color_scheme(const ColorScheme& scheme) noexcept {
|
||||
pimpl->set_color_scheme(*this, scheme);
|
||||
}
|
||||
void IconButton::load_theme_manager(ThemeManager& manager) noexcept {
|
||||
pimpl->load_theme_manager(*this, manager);
|
||||
}
|
||||
|
||||
void IconButton::enterEvent(qt::EnterEvent* event) {
|
||||
pimpl->enter_event(*this, *event);
|
||||
QAbstractButton::enterEvent(event);
|
||||
}
|
||||
void IconButton::leaveEvent(QEvent* event) {
|
||||
pimpl->leave_event(*this, *event);
|
||||
QAbstractButton::leaveEvent(event);
|
||||
}
|
||||
|
||||
void IconButton::paintEvent(QPaintEvent* event) { pimpl->paint_event(*this, *event); }
|
||||
|
||||
void IconButton::set_icon(const QString& icon) noexcept { pimpl->font_icon = icon; }
|
||||
void IconButton::set_icon(const QIcon& icon) noexcept { QAbstractButton::setIcon(icon); }
|
||||
|
||||
void IconButton::set_types(Types types) noexcept { pimpl->set_types_type(*this, types); }
|
||||
void IconButton::set_shape(Shape shape) noexcept { pimpl->set_shape_type(*this, shape); }
|
||||
void IconButton::set_color(Color color) noexcept { pimpl->set_color_type(*this, color); }
|
||||
void IconButton::set_width(Width width) noexcept { pimpl->set_width_type(*this, width); }
|
||||
|
||||
auto IconButton::types_enum() const noexcept -> Types { return pimpl->types; }
|
||||
auto IconButton::shape_enum() const noexcept -> Shape { return pimpl->shape; }
|
||||
auto IconButton::color_enum() const noexcept -> Color { return pimpl->color; }
|
||||
auto IconButton::width_enum() const noexcept -> Width { return pimpl->width; }
|
||||
|
||||
auto IconButton::selected() const noexcept -> bool {
|
||||
return pimpl->types == Types::TOGGLE_SELECTED;
|
||||
}
|
||||
auto IconButton::set_selected(bool selected) noexcept -> void {
|
||||
set_types(selected ? Types::TOGGLE_SELECTED : Types::TOGGLE_UNSELECTED);
|
||||
};
|
||||
133
creeper-qt/widget/buttons/icon-button.hh
Normal file
133
creeper-qt/widget/buttons/icon-button.hh
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include <qabstractbutton.h>
|
||||
#include <qpainter.h>
|
||||
|
||||
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
namespace creeper::icon_button::internal {
|
||||
class IconButton : public QAbstractButton {
|
||||
CREEPER_PIMPL_DEFINITION(IconButton);
|
||||
|
||||
public:
|
||||
enum class Types { DEFAULT, TOGGLE_UNSELECTED, TOGGLE_SELECTED };
|
||||
|
||||
enum class Shape { DEFAULT_ROUND, SQUARE };
|
||||
|
||||
enum class Color { DEFAULT_FILLED, TONAL, OUTLINED, STANDARD };
|
||||
|
||||
enum class Width { DEFAULT, NARROW, WIDE };
|
||||
|
||||
/// @brief
|
||||
/// 依照文档 https://m3.material.io/components/icon-buttons/specs
|
||||
/// 给出如下标准容器尺寸,图标尺寸和字体大小
|
||||
/// @note
|
||||
/// 该组件支持 Material Symbols,只要安装相关字体即可使用,下面的
|
||||
/// FontIcon Size 也是根据字体的大小而定的, utility/material-icon.hh
|
||||
/// 文件中有一些预定义的字体和图标编码
|
||||
|
||||
// Extra Small
|
||||
static constexpr auto kExtraSmallContainerSize = QSize { 32, 32 };
|
||||
static constexpr auto kExtraSmallIconSize = QSize { 20, 20 };
|
||||
static constexpr auto kExtraSmallFontIconSize = int { 15 };
|
||||
// Small
|
||||
static constexpr auto kSmallContainerSize = QSize { 40, 40 };
|
||||
static constexpr auto kSmallIconSize = QSize { 24, 24 };
|
||||
static constexpr auto kSmallFontIconSize = int { 18 };
|
||||
// Medium
|
||||
static constexpr auto kMediumContainerSize = QSize { 56, 56 };
|
||||
static constexpr auto kMediumIconSize = QSize { 24, 24 };
|
||||
static constexpr auto kMediumFontIconSize = int { 18 };
|
||||
// Large
|
||||
static constexpr auto kLargeContainerSize = QSize { 96, 96 };
|
||||
static constexpr auto kLargeIconSize = QSize { 32, 32 };
|
||||
static constexpr auto kLargeFontIconSize = int { 24 };
|
||||
// Extra Large
|
||||
static constexpr auto kExtraLargeContainerSize = QSize { 136, 136 };
|
||||
static constexpr auto kExtraLargeIconSize = QSize { 40, 40 };
|
||||
static constexpr auto kExtraLargeFontIconSize = int { 32 };
|
||||
|
||||
public:
|
||||
auto set_color_scheme(const ColorScheme&) noexcept -> void;
|
||||
auto load_theme_manager(ThemeManager&) noexcept -> void;
|
||||
|
||||
auto set_icon(const QString&) noexcept -> void;
|
||||
auto set_icon(const QIcon&) noexcept -> void;
|
||||
|
||||
auto set_types(Types) noexcept -> void;
|
||||
auto set_shape(Shape) noexcept -> void;
|
||||
auto set_color(Color) noexcept -> void;
|
||||
auto set_width(Width) noexcept -> void;
|
||||
|
||||
auto types_enum() const noexcept -> Types;
|
||||
auto shape_enum() const noexcept -> Shape;
|
||||
auto color_enum() const noexcept -> Color;
|
||||
auto width_enum() const noexcept -> Width;
|
||||
|
||||
auto selected() const noexcept -> bool;
|
||||
auto set_selected(bool) noexcept -> void;
|
||||
|
||||
// TODO: 详细的颜色自定义接口有缘再写
|
||||
|
||||
protected:
|
||||
auto enterEvent(qt::EnterEvent*) -> void override;
|
||||
auto leaveEvent(QEvent*) -> void override;
|
||||
|
||||
auto paintEvent(QPaintEvent*) -> void override;
|
||||
};
|
||||
}
|
||||
namespace creeper::icon_button::pro {
|
||||
using Token = common::Token<internal::IconButton>;
|
||||
|
||||
using Icon =
|
||||
creeper::DerivedProp<Token, QIcon, [](auto& self, const auto& v) { self.set_icon(v); }>;
|
||||
using FontIcon =
|
||||
creeper::DerivedProp<Token, QString, [](auto& self, const auto& v) { self.set_icon(v); }>;
|
||||
|
||||
using Color = creeper::SetterProp<Token, internal::IconButton::Color,
|
||||
[](auto& self, const auto& v) { self.set_color(v); }>;
|
||||
using Shape = creeper::SetterProp<Token, internal::IconButton::Shape,
|
||||
[](auto& self, const auto& v) { self.set_shape(v); }>;
|
||||
using Types = creeper::SetterProp<Token, internal::IconButton::Types,
|
||||
[](auto& self, const auto& v) { self.set_types(v); }>;
|
||||
using Width = creeper::SetterProp<Token, internal::IconButton::Width,
|
||||
[](auto& self, const auto& v) { self.set_width(v); }>;
|
||||
|
||||
constexpr auto ColorFilled = Color { internal::IconButton::Color::DEFAULT_FILLED };
|
||||
constexpr auto ColorOutlined = Color { internal::IconButton::Color::OUTLINED };
|
||||
constexpr auto ColorStandard = Color { internal::IconButton::Color::STANDARD };
|
||||
constexpr auto ColorTonal = Color { internal::IconButton::Color::TONAL };
|
||||
|
||||
constexpr auto ShapeRound = Shape { internal::IconButton::Shape::DEFAULT_ROUND };
|
||||
constexpr auto ShapeSquare = Shape { internal::IconButton::Shape::SQUARE };
|
||||
|
||||
constexpr auto TypesDefault = Types { internal::IconButton::Types::DEFAULT };
|
||||
constexpr auto TypesToggleSelected = Types { internal::IconButton::Types::TOGGLE_SELECTED };
|
||||
constexpr auto TypesToggleUnselected = Types { internal::IconButton::Types::TOGGLE_UNSELECTED };
|
||||
|
||||
constexpr auto WidthDefault = Width { internal::IconButton::Width::DEFAULT };
|
||||
constexpr auto WidthNarrow = Width { internal::IconButton::Width::NARROW };
|
||||
constexpr auto WidthWide = Width { internal::IconButton::Width::WIDE };
|
||||
|
||||
template <typename Callback>
|
||||
using Clickable = common::pro::Clickable<Callback, Token>;
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using IconButton = Declarative<icon_button::internal::IconButton,
|
||||
CheckerOr<icon_button::pro::checker, widget::pro::checker, theme::pro::checker>>;
|
||||
|
||||
}
|
||||
285
creeper-qt/widget/buttons/icon-button.impl.hh
Normal file
285
creeper-qt/widget/buttons/icon-button.impl.hh
Normal file
@@ -0,0 +1,285 @@
|
||||
#pragma once
|
||||
|
||||
#include "icon-button.hh"
|
||||
|
||||
#include "creeper-qt/utility/animation/animatable.hh"
|
||||
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||
#include "creeper-qt/utility/animation/state/spring.hh"
|
||||
#include "creeper-qt/utility/animation/transition.hh"
|
||||
#include "creeper-qt/utility/animation/water-ripple.hh"
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
|
||||
using namespace creeper::icon_button::internal;
|
||||
|
||||
constexpr auto kHoverOpacity = double { 0.1 };
|
||||
constexpr auto kWaterOpacity = double { 0.4 };
|
||||
constexpr auto kWidthRatio = double { 1.25 };
|
||||
constexpr auto kOutlineWidth = double { 1.5 };
|
||||
constexpr auto kSquareRatio = double { 0.5 };
|
||||
|
||||
constexpr double kp = 15.0, ki = 0.0, kd = 0.0;
|
||||
constexpr auto kSpringK = double { 400.0 };
|
||||
constexpr auto kSpringD = double { 15.0 };
|
||||
|
||||
constexpr auto kThreshold1D = double { 1e-1 };
|
||||
constexpr auto kWaterSpeed = double { 5.0 };
|
||||
|
||||
struct IconButton::Impl {
|
||||
|
||||
bool is_hovered = false;
|
||||
|
||||
QString font_icon {};
|
||||
|
||||
Types types { Types::DEFAULT };
|
||||
Shape shape { Shape::DEFAULT_ROUND };
|
||||
Color color { Color::DEFAULT_FILLED };
|
||||
Width width { Width::DEFAULT };
|
||||
|
||||
QColor container_color = Qt::white;
|
||||
QColor container_color_unselected = Qt::white;
|
||||
QColor container_color_selected = Qt::white;
|
||||
|
||||
QColor outline_color = Qt::gray;
|
||||
QColor outline_color_unselected = Qt::gray;
|
||||
QColor outline_color_selected = Qt::gray;
|
||||
|
||||
QColor icon_color = Qt::black;
|
||||
QColor icon_color_unselected = Qt::black;
|
||||
QColor icon_color_selected = Qt::black;
|
||||
|
||||
QColor hover_color = Qt::gray;
|
||||
QColor hover_color_unselected = Qt::gray;
|
||||
QColor hover_color_selected = Qt::gray;
|
||||
|
||||
QColor water_color = Qt::gray;
|
||||
|
||||
Animatable animatable;
|
||||
WaterRippleRenderer water_ripple;
|
||||
|
||||
std::unique_ptr<TransitionValue<SpringState<double>>> now_container_radius;
|
||||
std::unique_ptr<TransitionValue<PidState<Eigen::Vector4d>>> now_color_container;
|
||||
std::unique_ptr<TransitionValue<PidState<Eigen::Vector4d>>> now_color_icon;
|
||||
std::unique_ptr<TransitionValue<PidState<Eigen::Vector4d>>> now_color_outline;
|
||||
|
||||
explicit Impl(IconButton& self) noexcept
|
||||
: animatable { self }
|
||||
, water_ripple { animatable, kWaterSpeed } {
|
||||
|
||||
{
|
||||
auto state = std::make_shared<SpringState<double>>();
|
||||
|
||||
state->config.epsilon = kThreshold1D;
|
||||
state->config.k = kSpringK;
|
||||
state->config.d = kSpringD;
|
||||
|
||||
now_container_radius = make_transition(animatable, std::move(state));
|
||||
}
|
||||
{
|
||||
constexpr auto make_state = [] {
|
||||
auto state = std::make_shared<PidState<Eigen::Vector4d>>();
|
||||
|
||||
state->config.kp = kp;
|
||||
state->config.ki = ki;
|
||||
state->config.kd = kd;
|
||||
return state;
|
||||
};
|
||||
now_color_container = make_transition(animatable, make_state());
|
||||
now_color_icon = make_transition(animatable, make_state());
|
||||
now_color_outline = make_transition(animatable, make_state());
|
||||
}
|
||||
|
||||
QObject::connect(&self, &IconButton::clicked, [this, &self] {
|
||||
if (types == Types::DEFAULT) {
|
||||
const auto center_point = self.mapFromGlobal(QCursor::pos());
|
||||
const auto max_distance = std::max(self.width(), self.height());
|
||||
water_ripple.clicked(center_point, max_distance);
|
||||
}
|
||||
|
||||
toggle_status();
|
||||
update_animation_status(self);
|
||||
});
|
||||
}
|
||||
|
||||
auto enter_event(IconButton& self, const QEvent& event) {
|
||||
self.setCursor(Qt::PointingHandCursor);
|
||||
is_hovered = true;
|
||||
}
|
||||
|
||||
auto leave_event(IconButton& self, const QEvent& event) { is_hovered = false; }
|
||||
|
||||
auto paint_event(IconButton& self, const QPaintEvent& event) {
|
||||
// TODO: 做计算数据缓存优化,特别是 Resize 相关的计算
|
||||
const auto icon = self.icon();
|
||||
|
||||
const auto color_container = from_vector4(*now_color_container);
|
||||
const auto color_icon = from_vector4(*now_color_icon);
|
||||
const auto color_outline = from_vector4(*now_color_outline);
|
||||
|
||||
const auto hover_color = is_hovered ? get_hover_color() : Qt::transparent;
|
||||
|
||||
const auto container_radius = *now_container_radius;
|
||||
const auto container_rect = container_rectangle(self);
|
||||
|
||||
auto clip_path = QPainterPath {};
|
||||
clip_path.addRoundedRect(container_rect, container_radius, container_radius);
|
||||
|
||||
auto renderer = QPainter { &self };
|
||||
util::PainterHelper { renderer }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color_container, color_outline, kOutlineWidth, container_rect,
|
||||
container_radius, container_radius)
|
||||
.apply(water_ripple.renderer(clip_path, water_color))
|
||||
.simple_text(font_icon, self.font(), color_icon, container_rect, Qt::AlignCenter)
|
||||
.rounded_rectangle(
|
||||
hover_color, Qt::transparent, 0, container_rect, container_radius, container_radius)
|
||||
.done();
|
||||
}
|
||||
|
||||
auto set_types_type(IconButton& self, Types types) {
|
||||
this->types = types;
|
||||
update_animation_status(self);
|
||||
}
|
||||
|
||||
auto set_color_type(IconButton& self, Color color) {
|
||||
this->color = color;
|
||||
update_animation_status(self);
|
||||
}
|
||||
|
||||
auto set_shape_type(IconButton& self, Shape shape) {
|
||||
this->shape = shape;
|
||||
update_animation_status(self);
|
||||
}
|
||||
|
||||
auto set_width_type(IconButton& self, Width width) {
|
||||
this->width = width;
|
||||
update_animation_status(self);
|
||||
}
|
||||
|
||||
auto set_color_scheme(IconButton& self, const ColorScheme& scheme) {
|
||||
switch (color) {
|
||||
case Color::DEFAULT_FILLED:
|
||||
container_color = scheme.primary;
|
||||
icon_color = scheme.on_primary;
|
||||
outline_color = Qt::transparent;
|
||||
|
||||
container_color_unselected = scheme.surface_container_high;
|
||||
icon_color_unselected = scheme.primary;
|
||||
outline_color_unselected = Qt::transparent;
|
||||
|
||||
container_color_selected = scheme.primary;
|
||||
icon_color_selected = scheme.on_primary;
|
||||
outline_color_selected = Qt::transparent;
|
||||
break;
|
||||
case Color::TONAL:
|
||||
container_color = scheme.secondary_container;
|
||||
icon_color = scheme.on_secondary_container;
|
||||
outline_color = Qt::transparent;
|
||||
|
||||
container_color_unselected = scheme.surface_container_high;
|
||||
icon_color_unselected = scheme.surface_variant;
|
||||
outline_color_unselected = Qt::transparent;
|
||||
|
||||
container_color_selected = scheme.secondary_container;
|
||||
icon_color_selected = scheme.on_secondary_container;
|
||||
outline_color_selected = Qt::transparent;
|
||||
break;
|
||||
case Color::OUTLINED:
|
||||
container_color = Qt::transparent;
|
||||
outline_color = scheme.outline_variant;
|
||||
icon_color = scheme.on_surface_variant;
|
||||
|
||||
container_color_unselected = Qt::transparent;
|
||||
outline_color_unselected = scheme.outline_variant;
|
||||
icon_color_unselected = scheme.surface_variant;
|
||||
|
||||
container_color_selected = scheme.inverse_surface;
|
||||
outline_color_selected = scheme.inverse_surface;
|
||||
icon_color_selected = scheme.inverse_on_surface;
|
||||
break;
|
||||
case Color::STANDARD:
|
||||
container_color = Qt::transparent;
|
||||
outline_color = Qt::transparent;
|
||||
icon_color = scheme.on_surface_variant;
|
||||
|
||||
container_color_unselected = Qt::transparent;
|
||||
outline_color_unselected = Qt::transparent;
|
||||
icon_color_unselected = scheme.on_surface_variant;
|
||||
|
||||
container_color_selected = Qt::transparent;
|
||||
outline_color_selected = Qt::transparent;
|
||||
icon_color_selected = scheme.primary;
|
||||
break;
|
||||
}
|
||||
|
||||
hover_color = icon_color;
|
||||
hover_color.setAlphaF(kHoverOpacity);
|
||||
|
||||
hover_color_selected = icon_color_selected;
|
||||
hover_color_selected.setAlphaF(kHoverOpacity);
|
||||
|
||||
hover_color_unselected = icon_color_unselected;
|
||||
hover_color_unselected.setAlphaF(kHoverOpacity);
|
||||
|
||||
water_color = icon_color;
|
||||
water_color.setAlphaF(kWaterOpacity);
|
||||
|
||||
update_animation_status(self);
|
||||
}
|
||||
|
||||
auto load_theme_manager(IconButton& self, ThemeManager& manager) {
|
||||
manager.append_handler(&self, [this, &self](const ThemeManager& manager) {
|
||||
set_color_scheme(self, manager.color_scheme());
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
auto update_animation_status(IconButton& self) -> void {
|
||||
|
||||
const auto container_color_target = (types == Types::DEFAULT) ? container_color : //
|
||||
(types == Types::TOGGLE_SELECTED) ? container_color_selected
|
||||
: container_color_unselected;
|
||||
now_color_container->transition_to(from_color(container_color_target));
|
||||
|
||||
const auto icon_color_target = (types == Types::DEFAULT) ? icon_color : //
|
||||
(types == Types::TOGGLE_SELECTED) ? icon_color_selected
|
||||
: icon_color_unselected;
|
||||
now_color_icon->transition_to(from_color(icon_color_target));
|
||||
|
||||
const auto outline_color_target //
|
||||
= (types == Types::DEFAULT) ? outline_color
|
||||
: (types == Types::TOGGLE_SELECTED) ? outline_color_selected
|
||||
: outline_color_unselected;
|
||||
now_color_outline->transition_to(from_color(outline_color_target));
|
||||
|
||||
const auto rectangle = container_rectangle(self);
|
||||
const auto radius_round = std::min<double>(rectangle.width(), rectangle.height()) / 2.;
|
||||
const auto radius_target = (types == Types::TOGGLE_SELECTED || shape == Shape::SQUARE)
|
||||
? radius_round * kSquareRatio
|
||||
: radius_round * 1.0;
|
||||
now_container_radius->transition_to(radius_target);
|
||||
}
|
||||
|
||||
auto get_hover_color() const noexcept -> QColor {
|
||||
switch (types) {
|
||||
case Types::DEFAULT:
|
||||
return hover_color;
|
||||
case Types::TOGGLE_UNSELECTED:
|
||||
return hover_color_unselected;
|
||||
case Types::TOGGLE_SELECTED:
|
||||
return hover_color_selected;
|
||||
}
|
||||
return { /* 不可能到达的彼岸 */ };
|
||||
}
|
||||
|
||||
auto toggle_status() -> void {
|
||||
if (types == Types::TOGGLE_UNSELECTED) types = Types::TOGGLE_SELECTED;
|
||||
else if (types == Types::TOGGLE_SELECTED) types = Types::TOGGLE_UNSELECTED;
|
||||
}
|
||||
|
||||
// 设计指南上的大小全是固定的,十分不自由,故转成比例
|
||||
auto container_rectangle(IconButton& self) -> QRectF {
|
||||
return (width == Width::DEFAULT) ? (extract_rect(self.rect(), 1, 1))
|
||||
: (width == Width::NARROW) ? (extract_rect(self.rect(), 1, kWidthRatio))
|
||||
: (extract_rect(self.rect(), kWidthRatio, 1));
|
||||
}
|
||||
};
|
||||
43
creeper-qt/widget/buttons/outlined-button.hh
Normal file
43
creeper-qt/widget/buttons/outlined-button.hh
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include "filled-button.hh"
|
||||
|
||||
namespace creeper::outlined_button::internal {
|
||||
class OutlinedButton : public FilledButton {
|
||||
public:
|
||||
void set_color_scheme(const ColorScheme& color_scheme) {
|
||||
set_background(Qt::transparent);
|
||||
set_border_color(color_scheme.outline);
|
||||
set_text_color(color_scheme.primary);
|
||||
|
||||
auto hover_color = color_scheme.primary;
|
||||
hover_color.setAlphaF(0.08);
|
||||
set_hover_color(hover_color);
|
||||
|
||||
auto water_color = QColor {};
|
||||
if (color_scheme.primary.lightness() > 128) {
|
||||
water_color = color_scheme.primary.darker(130);
|
||||
} else {
|
||||
water_color = color_scheme.primary.lighter(130);
|
||||
}
|
||||
water_color.setAlphaF(0.25);
|
||||
set_water_color(water_color);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& manager) noexcept -> void {
|
||||
set_border_width(1.5);
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
};
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
namespace outlined_button::pro {
|
||||
using namespace filled_button::pro;
|
||||
}
|
||||
|
||||
using OutlinedButton =
|
||||
Declarative<outlined_button::internal::OutlinedButton, FilledButton::Checker>;
|
||||
}
|
||||
41
creeper-qt/widget/buttons/text-button.hh
Normal file
41
creeper-qt/widget/buttons/text-button.hh
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include "filled-button.hh"
|
||||
|
||||
namespace creeper::text_button::internal {
|
||||
class TextButton : public FilledButton {
|
||||
public:
|
||||
void set_color_scheme(const ColorScheme& color_scheme) {
|
||||
set_background(Qt::transparent);
|
||||
set_text_color(color_scheme.primary);
|
||||
|
||||
auto hover_color = color_scheme.primary;
|
||||
hover_color.setAlphaF(0.08);
|
||||
set_hover_color(hover_color);
|
||||
|
||||
auto water_color = QColor {};
|
||||
if (color_scheme.primary.lightness() > 128) {
|
||||
water_color = color_scheme.primary.darker(130);
|
||||
} else {
|
||||
water_color = color_scheme.primary.lighter(130);
|
||||
}
|
||||
water_color.setAlphaF(0.25);
|
||||
set_water_color(water_color);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
namespace creeper {
|
||||
|
||||
namespace text_button::pro {
|
||||
using namespace filled_button::pro;
|
||||
}
|
||||
|
||||
using TextButton = Declarative<text_button::internal::TextButton, FilledButton::Checker>;
|
||||
}
|
||||
97
creeper-qt/widget/cards/basic-card.hh
Normal file
97
creeper-qt/widget/cards/basic-card.hh
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/widget/shape/rounded-rect.hh"
|
||||
|
||||
namespace creeper::card::internal {
|
||||
|
||||
constexpr auto kCardRadius = double { 12 };
|
||||
|
||||
constexpr auto kElevatedShadowOpacity = double { 0.4 };
|
||||
constexpr auto kElevatedShadowBlurRadius = double { 10 };
|
||||
constexpr auto kElevatedShadowOffsetX = double { 0 };
|
||||
constexpr auto kElevatedShadowOffsetY = double { 2 };
|
||||
|
||||
constexpr auto kOutlinedWidth = double { 1.5 };
|
||||
|
||||
class Card : public RoundedRect {
|
||||
public:
|
||||
enum class Level {
|
||||
LOWEST,
|
||||
LOW,
|
||||
DEFAULT,
|
||||
HIGH,
|
||||
HIGHEST,
|
||||
};
|
||||
|
||||
Level level = Level::DEFAULT;
|
||||
|
||||
public:
|
||||
explicit Card() noexcept
|
||||
: Declarative {
|
||||
rounded_rect::pro::BorderWidth { 0 },
|
||||
rounded_rect::pro::BorderColor { Qt::transparent },
|
||||
rounded_rect::pro::Radius { kCardRadius },
|
||||
} { }
|
||||
|
||||
auto set_level(Level level) noexcept {
|
||||
this->level = level;
|
||||
update();
|
||||
}
|
||||
|
||||
void set_color_scheme(const ColorScheme& scheme) {
|
||||
switch (level) {
|
||||
case card::internal::Card::Level::LOWEST:
|
||||
set_background(scheme.surface_container_lowest);
|
||||
break;
|
||||
case card::internal::Card::Level::LOW:
|
||||
set_background(scheme.surface_container_low);
|
||||
break;
|
||||
case card::internal::Card::Level::DEFAULT:
|
||||
set_background(scheme.surface_container);
|
||||
break;
|
||||
case card::internal::Card::Level::HIGH:
|
||||
set_background(scheme.surface_container_high);
|
||||
break;
|
||||
case card::internal::Card::Level::HIGHEST:
|
||||
set_background(scheme.surface_container_highest);
|
||||
break;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
};
|
||||
}
|
||||
namespace creeper::card::pro {
|
||||
|
||||
using Token = common::Token<internal::Card>;
|
||||
|
||||
using Level =
|
||||
SetterProp<Token, internal::Card::Level, [](auto& self, const auto& v) { self.set_level(v); }>;
|
||||
|
||||
constexpr auto LevelDefault = Level { internal::Card::Level::DEFAULT };
|
||||
constexpr auto LevelHigh = Level { internal::Card::Level::HIGH };
|
||||
constexpr auto LevelHighest = Level { internal::Card::Level::HIGHEST };
|
||||
constexpr auto LevelLow = Level { internal::Card::Level::LOW };
|
||||
constexpr auto LevelLowest = Level { internal::Card::Level::LOWEST };
|
||||
|
||||
template <class Card>
|
||||
concept trait = std::derived_from<Card, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
|
||||
using namespace rounded_rect::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using CardLevel = card::internal::Card::Level;
|
||||
|
||||
using BasicCard = Declarative<card::internal::Card,
|
||||
CheckerOr<card::pro::checker, rounded_rect::pro::checker, theme::pro::checker,
|
||||
widget::pro::checker>>;
|
||||
|
||||
}
|
||||
39
creeper-qt/widget/cards/elevated-card.hh
Normal file
39
creeper-qt/widget/cards/elevated-card.hh
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
#include "basic-card.hh"
|
||||
|
||||
namespace creeper {
|
||||
namespace elevated_card::internal {
|
||||
class ElevatedCard : public BasicCard {
|
||||
public:
|
||||
explicit ElevatedCard() {
|
||||
using namespace card::internal;
|
||||
shadow_effect.setBlurRadius(kElevatedShadowBlurRadius);
|
||||
shadow_effect.setOffset(kElevatedShadowOffsetX, kElevatedShadowOffsetY);
|
||||
setGraphicsEffect(&shadow_effect);
|
||||
}
|
||||
|
||||
void set_color_scheme(const ColorScheme& scheme) {
|
||||
using namespace card::internal;
|
||||
|
||||
auto shadow_color = scheme.shadow;
|
||||
shadow_color.setAlphaF(kElevatedShadowOpacity);
|
||||
|
||||
shadow_effect.setColor(shadow_color);
|
||||
Card::set_color_scheme(scheme);
|
||||
}
|
||||
|
||||
void load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
|
||||
private:
|
||||
QGraphicsDropShadowEffect shadow_effect {};
|
||||
};
|
||||
|
||||
}
|
||||
namespace elevated_card::pro {
|
||||
using namespace card::pro;
|
||||
}
|
||||
using ElevatedCard = Declarative<elevated_card::internal::ElevatedCard, BasicCard::Checker>;
|
||||
}
|
||||
9
creeper-qt/widget/cards/filled-card.hh
Normal file
9
creeper-qt/widget/cards/filled-card.hh
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include "basic-card.hh"
|
||||
|
||||
namespace creeper {
|
||||
namespace filled_card::pro {
|
||||
using namespace card::pro;
|
||||
}
|
||||
using FilledCard = Declarative<BasicCard, BasicCard::Checker>;
|
||||
}
|
||||
29
creeper-qt/widget/cards/outlined-card.hh
Normal file
29
creeper-qt/widget/cards/outlined-card.hh
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "basic-card.hh"
|
||||
|
||||
namespace creeper {
|
||||
namespace outlined_card::internal {
|
||||
class OutlinedCard : public BasicCard {
|
||||
public:
|
||||
explicit OutlinedCard() {
|
||||
using namespace card::internal;
|
||||
set_border_width(kOutlinedWidth);
|
||||
}
|
||||
|
||||
void set_color_scheme(const ColorScheme& scheme) {
|
||||
set_border_color(scheme.outline_variant);
|
||||
Card::set_color_scheme(scheme);
|
||||
}
|
||||
|
||||
void load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
namespace outlined_card::pro {
|
||||
using namespace card::pro;
|
||||
}
|
||||
using OutlinedCard = Declarative<outlined_card::internal::OutlinedCard, BasicCard::Checker>;
|
||||
}
|
||||
61
creeper-qt/widget/dropdown-menu.cc
Normal file
61
creeper-qt/widget/dropdown-menu.cc
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "dropdown-menu.impl.hh"
|
||||
|
||||
DropdownMenu::DropdownMenu()
|
||||
: pimpl { std::make_unique<Impl>(*this) } { }
|
||||
|
||||
DropdownMenu::~DropdownMenu() = default;
|
||||
|
||||
void DropdownMenu::set_color_scheme(const ColorScheme& scheme) { pimpl->set_color_scheme(scheme); }
|
||||
|
||||
void DropdownMenu::load_theme_manager(ThemeManager& manager) { pimpl->load_theme_manager(manager); }
|
||||
|
||||
void DropdownMenu::set_label_text(const QString& text) { pimpl->set_label_text(text); }
|
||||
|
||||
void DropdownMenu::set_leading_icon(const QIcon&) { }
|
||||
|
||||
void DropdownMenu::set_leading_icon(const QString& code, const QString& font) {
|
||||
pimpl->set_leading_icon(code, font);
|
||||
}
|
||||
|
||||
void DropdownMenu::resizeEvent(QResizeEvent* event) { QComboBox::resizeEvent(event); }
|
||||
|
||||
void DropdownMenu::enterEvent(qt::EnterEvent* enter_event) {
|
||||
pimpl->enter_event(enter_event);
|
||||
QComboBox::enterEvent(enter_event);
|
||||
}
|
||||
|
||||
void DropdownMenu::leaveEvent(QEvent* event) {
|
||||
pimpl->leave_event(event);
|
||||
QComboBox::leaveEvent(event);
|
||||
}
|
||||
|
||||
void DropdownMenu::focusInEvent(QFocusEvent* focus_event) {
|
||||
pimpl->focus_in(focus_event);
|
||||
QComboBox::focusInEvent(focus_event);
|
||||
}
|
||||
|
||||
void DropdownMenu::focusOutEvent(QFocusEvent* event) {
|
||||
pimpl->focus_out(event);
|
||||
QComboBox::focusOutEvent(event);
|
||||
}
|
||||
|
||||
void DropdownMenu::changeEvent(QEvent* event) { QComboBox::changeEvent(event); }
|
||||
|
||||
void DropdownMenu::showPopup() { pimpl->show_popup(); }
|
||||
|
||||
void DropdownMenu::hidePopup() { pimpl->hide_popup(); }
|
||||
|
||||
auto DropdownMenu::set_measurements(const Measurements& measurements) noexcept -> void {
|
||||
pimpl->set_measurements(measurements);
|
||||
}
|
||||
|
||||
void DropdownMenu::setTextMargins(const QMargins& margins) { this->margins = margins; }
|
||||
|
||||
QMargins DropdownMenu::textMargins() const { return margins; }
|
||||
|
||||
using namespace creeper;
|
||||
|
||||
void FilledDropdownMenu::paintEvent(QPaintEvent* event) {
|
||||
pimpl->paint_filled(event);
|
||||
// QComboBox::paintEvent(event);
|
||||
}
|
||||
160
creeper-qt/widget/dropdown-menu.hh
Normal file
160
creeper-qt/widget/dropdown-menu.hh
Normal file
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
#include <qcombobox.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
class FilledDropdownMenu;
|
||||
|
||||
namespace dropdown_menu::internal {
|
||||
|
||||
class DropdownMenu : public QComboBox {
|
||||
CREEPER_PIMPL_DEFINITION(DropdownMenu);
|
||||
friend FilledDropdownMenu;
|
||||
|
||||
public:
|
||||
struct ColorSpace {
|
||||
struct Tokens {
|
||||
QColor container;
|
||||
QColor caret;
|
||||
QColor active_indicator;
|
||||
|
||||
QColor input_text;
|
||||
QColor label_text;
|
||||
QColor selected_text;
|
||||
QColor supporting_text;
|
||||
|
||||
QColor leading_icon;
|
||||
QColor outline;
|
||||
|
||||
QColor itemlist_bg;
|
||||
QColor item_bg;
|
||||
QColor item_selected_bg;
|
||||
QColor item_hovered_bg;
|
||||
};
|
||||
|
||||
Tokens enabled;
|
||||
Tokens disabled;
|
||||
Tokens focused;
|
||||
Tokens error;
|
||||
|
||||
QColor state_layer;
|
||||
QColor selection_container;
|
||||
};
|
||||
|
||||
struct Measurements {
|
||||
int container_height = 56;
|
||||
|
||||
int icon_rect_size = 24;
|
||||
int input_rect_size = 24;
|
||||
int label_rect_size = 24;
|
||||
|
||||
int standard_font_height = 18;
|
||||
|
||||
int col_padding = 8;
|
||||
int row_padding_widthout_icons = 16;
|
||||
int row_padding_with_icons = 12;
|
||||
int row_padding_populated_label_text = 4;
|
||||
|
||||
int padding_icons_text = 16;
|
||||
|
||||
int supporting_text_and_character_counter_top_padding = 4;
|
||||
int supporting_text_and_character_counter_row_padding = 16;
|
||||
|
||||
auto icon_size() const -> QSize { return QSize { icon_rect_size, icon_rect_size }; };
|
||||
};
|
||||
auto set_color_scheme(const ColorScheme&) -> void;
|
||||
|
||||
void load_theme_manager(ThemeManager&);
|
||||
|
||||
void set_label_text(const QString&);
|
||||
|
||||
void set_leading_icon(const QIcon&);
|
||||
|
||||
void set_leading_icon(const QString& code, const QString& font);
|
||||
|
||||
auto set_measurements(const Measurements&) noexcept -> void;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
void enterEvent(qt::EnterEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
void focusInEvent(QFocusEvent*) override;
|
||||
void focusOutEvent(QFocusEvent* event) override;
|
||||
|
||||
void changeEvent(QEvent* event) override;
|
||||
|
||||
void showPopup() override;
|
||||
void hidePopup() override;
|
||||
|
||||
private:
|
||||
friend struct Impl;
|
||||
|
||||
public:
|
||||
void setTextMargins(const QMargins& margins);
|
||||
QMargins textMargins() const;
|
||||
|
||||
private:
|
||||
QMargins margins { 13, 24, 13, 0 };
|
||||
};
|
||||
}
|
||||
|
||||
namespace dropdown_menu::pro {
|
||||
|
||||
using Token = common::Token<internal::DropdownMenu>;
|
||||
|
||||
using LabelText = common::pro::String<Token,
|
||||
[](auto& self, const auto& string) { self.set_label_text(string); }>;
|
||||
|
||||
struct LeadingIcon : Token {
|
||||
QString code;
|
||||
QString font;
|
||||
explicit LeadingIcon(const QString& code, const QString& font)
|
||||
: code { code }
|
||||
, font { font } { }
|
||||
void apply(auto& self) const { self.set_leading_icon(code, font); }
|
||||
};
|
||||
|
||||
/// @note: currentIndexChanged(int index)
|
||||
template <typename F>
|
||||
using IndexChanged =
|
||||
common::pro::SignalInjection<F, Token, &internal::DropdownMenu::currentIndexChanged>;
|
||||
|
||||
template <typename F>
|
||||
using TextChanged = common::pro::SignalInjection<F, Token, &internal::DropdownMenu::currentTextChanged>;
|
||||
|
||||
using Items = DerivedProp<Token, QVector<QString>, //
|
||||
[](auto& self, const auto& vec) {
|
||||
self.clear();
|
||||
self.addItems(vec);
|
||||
self.setCurrentIndex(-1);
|
||||
}>;
|
||||
|
||||
template <class Select>
|
||||
concept trait = std::derived_from<Select, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
|
||||
struct FilledDropdownMenu
|
||||
: public Declarative<dropdown_menu::internal::DropdownMenu,
|
||||
CheckerOr<dropdown_menu::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||
using Declarative::Declarative;
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
};
|
||||
namespace filled_dropdown_menu::pro {
|
||||
using namespace dropdown_menu::pro;
|
||||
}
|
||||
|
||||
}
|
||||
386
creeper-qt/widget/dropdown-menu.impl.hh
Normal file
386
creeper-qt/widget/dropdown-menu.impl.hh
Normal file
@@ -0,0 +1,386 @@
|
||||
/// 显然的,原生 QComboBox 的下拉列表样式并不符合 Material Design
|
||||
/// 规范,未来必须切换成自定义的组件,相关参考:
|
||||
/// - https://m3.material.io/components/menus/guidelines
|
||||
/// - https://api.flutter.dev/flutter/material/DropdownMenu-class.html
|
||||
|
||||
#pragma once
|
||||
#include "dropdown-menu.hh"
|
||||
|
||||
#include "creeper-qt/utility/animation/animatable.hh"
|
||||
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||
#include "creeper-qt/utility/animation/transition.hh"
|
||||
#include "creeper-qt/utility/material-icon.hh"
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
|
||||
#include <qabstractitemview.h>
|
||||
#include <qfontmetrics.h>
|
||||
|
||||
using namespace creeper::dropdown_menu::internal;
|
||||
|
||||
struct DropdownMenu::Impl {
|
||||
public:
|
||||
explicit Impl(DropdownMenu& self) noexcept
|
||||
: animatable(self)
|
||||
, self { 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_space.enabled.container = scheme.surface_container_highest;
|
||||
color_space.enabled.label_text = scheme.on_surface_variant;
|
||||
color_space.enabled.selected_text = scheme.on_surface;
|
||||
color_space.enabled.leading_icon = scheme.on_surface_variant;
|
||||
color_space.enabled.active_indicator = scheme.on_surface_variant;
|
||||
color_space.enabled.supporting_text = scheme.on_surface_variant;
|
||||
color_space.enabled.input_text = scheme.on_surface;
|
||||
color_space.enabled.caret = scheme.primary;
|
||||
color_space.enabled.outline = scheme.outline;
|
||||
|
||||
color_space.disabled.container = scheme.on_surface;
|
||||
color_space.disabled.container.setAlphaF(0.04);
|
||||
color_space.disabled.label_text = scheme.on_surface;
|
||||
color_space.disabled.label_text.setAlphaF(0.38);
|
||||
color_space.disabled.selected_text = scheme.on_surface;
|
||||
color_space.disabled.selected_text.setAlphaF(0.38);
|
||||
color_space.disabled.leading_icon = scheme.on_surface;
|
||||
color_space.disabled.leading_icon.setAlphaF(0.38);
|
||||
color_space.disabled.supporting_text = scheme.on_surface;
|
||||
color_space.disabled.supporting_text.setAlphaF(0.38);
|
||||
color_space.disabled.input_text = scheme.on_surface;
|
||||
color_space.disabled.input_text.setAlphaF(0.38);
|
||||
color_space.disabled.active_indicator = scheme.on_surface;
|
||||
color_space.disabled.active_indicator.setAlphaF(0.38);
|
||||
color_space.disabled.outline = scheme.outline;
|
||||
color_space.disabled.outline.setAlphaF(0.38);
|
||||
|
||||
color_space.focused.container = scheme.surface_container_highest;
|
||||
color_space.focused.label_text = scheme.primary;
|
||||
color_space.focused.selected_text = scheme.on_surface;
|
||||
color_space.focused.leading_icon = scheme.on_surface_variant;
|
||||
color_space.focused.input_text = scheme.on_surface;
|
||||
color_space.focused.supporting_text = scheme.on_surface_variant;
|
||||
color_space.focused.active_indicator = scheme.primary;
|
||||
color_space.focused.outline = scheme.primary;
|
||||
|
||||
color_space.error.container = scheme.surface_container_highest;
|
||||
color_space.error.active_indicator = scheme.error;
|
||||
color_space.error.label_text = scheme.error;
|
||||
color_space.error.selected_text = scheme.on_surface;
|
||||
color_space.error.input_text = scheme.on_surface;
|
||||
color_space.error.supporting_text = scheme.error;
|
||||
color_space.error.leading_icon = scheme.on_surface_variant;
|
||||
color_space.error.caret = scheme.error;
|
||||
color_space.error.outline = scheme.error;
|
||||
|
||||
color_space.state_layer = scheme.on_surface;
|
||||
color_space.state_layer.setAlphaF(0.08);
|
||||
|
||||
const auto& color = get_color_tokens();
|
||||
sync_basic_text_style(color.input_text, scheme.surface_container_highest, color.input_text,
|
||||
color_space.state_layer);
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(&self,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
|
||||
auto set_label_text(const QString& text) { label_text = text; }
|
||||
|
||||
auto set_leading_icon(const QString& code, const QString& font) {
|
||||
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 widget_rect = self.rect();
|
||||
const auto color = get_color_tokens();
|
||||
|
||||
constexpr auto container_radius = 5;
|
||||
update_component_status();
|
||||
|
||||
auto painter = QPainter { &self };
|
||||
|
||||
// Draw container with fixed measurements height and vertically centered
|
||||
const auto container_rect = QRect { widget_rect.left(),
|
||||
widget_rect.top() + (widget_rect.height() - measurements.container_height) / 2,
|
||||
widget_rect.width(), measurements.container_height };
|
||||
|
||||
{
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color.container, Qt::transparent, 0, container_rect,
|
||||
container_radius, container_radius, 0, 0);
|
||||
}
|
||||
|
||||
// Active indicator at container bottom
|
||||
{
|
||||
const auto p0 = container_rect.bottomLeft();
|
||||
const auto p1 = container_rect.bottomRight();
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen({ color.active_indicator, filled_line_width() });
|
||||
painter.drawLine(p0, p1);
|
||||
}
|
||||
|
||||
// Icon positioned relative to container_rect
|
||||
const auto rect_icon = QRectF {
|
||||
container_rect.right() - self.textMargins().right() - measurements.icon_rect_size * 1.,
|
||||
container_rect.top() + (container_rect.height() - measurements.icon_rect_size) * 0.5,
|
||||
1. * measurements.icon_rect_size,
|
||||
1. * measurements.icon_rect_size,
|
||||
};
|
||||
const auto icon_center = rect_icon.center();
|
||||
const bool is_active = (self.view() && self.view()->isVisible());
|
||||
|
||||
painter.save();
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen { color.leading_icon });
|
||||
painter.setFont(leading_icon_font);
|
||||
painter.translate(icon_center);
|
||||
painter.rotate(is_active ? 180.0 : 0.0);
|
||||
painter.translate(-icon_center);
|
||||
painter.drawText(rect_icon, leading_icon_code, { Qt::AlignCenter });
|
||||
painter.restore();
|
||||
|
||||
if (!label_text.isEmpty()) {
|
||||
const auto margins = self.textMargins();
|
||||
|
||||
const auto center_label_y = container_rect.top()
|
||||
+ (measurements.container_height - measurements.label_rect_size) / 2.0;
|
||||
|
||||
const auto rect_center = QRectF {
|
||||
QPointF { static_cast<double>(margins.left()), center_label_y },
|
||||
QPointF(container_rect.right() - margins.right(),
|
||||
center_label_y + measurements.label_rect_size),
|
||||
};
|
||||
|
||||
const auto rect_top = QRectF {
|
||||
QPointF(margins.left(), container_rect.top() + measurements.col_padding),
|
||||
QPointF(container_rect.right() - margins.right(),
|
||||
container_rect.top() + measurements.col_padding + measurements.label_rect_size),
|
||||
};
|
||||
|
||||
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;
|
||||
const auto label_anchor = QPointF { label_rect.left(), label_rect.center().y() };
|
||||
|
||||
painter.save();
|
||||
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) {
|
||||
painter.save();
|
||||
// Place selected text in the input area (below the floating label)
|
||||
const auto input_top =
|
||||
container_rect.top() + measurements.col_padding + measurements.label_rect_size;
|
||||
const auto input_bottom = container_rect.bottom() - measurements.col_padding;
|
||||
const auto rect_center_selected = QRectF {
|
||||
QPointF { static_cast<double>(margins.left()), static_cast<double>(input_top) },
|
||||
QPointF(container_rect.right() - margins.right(),
|
||||
static_cast<double>(input_bottom)),
|
||||
};
|
||||
|
||||
// Draw selected text with input text color
|
||||
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 margins = self.textMargins();
|
||||
const auto input_top = container_rect.top()
|
||||
+ (container_rect.height() - measurements.input_rect_size) / 2.0;
|
||||
const auto input_bottom = input_top + measurements.input_rect_size;
|
||||
const auto rect_selected = QRectF {
|
||||
QPointF(margins.left(), input_top),
|
||||
QPointF(container_rect.right() - margins.right(), input_bottom),
|
||||
};
|
||||
|
||||
// Draw selected text
|
||||
painter.save();
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen { color.selected_text });
|
||||
painter.setFont(standard_text_font);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.drawText(
|
||||
rect_selected, self.currentText(), Qt::AlignVCenter | Qt::AlignLeading);
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
if (is_hovered) {
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color_space.state_layer, Qt::transparent, 0, container_rect,
|
||||
container_radius, container_radius, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
auto enter_event(qt::EnterEvent*) {
|
||||
is_hovered = true;
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto leave_event(QEvent*) {
|
||||
is_hovered = false;
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto focus_in(QFocusEvent*) {
|
||||
is_focused = true;
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto focus_out(QFocusEvent*) {
|
||||
is_focused = false;
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto show_popup() {
|
||||
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) -> 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 {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
selection-color: %3;
|
||||
selection-background-color: %4;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
color: %1;
|
||||
background-color: %2;
|
||||
}
|
||||
)";
|
||||
|
||||
self.setStyleSheet(QString { kQComboBoxStyle }
|
||||
.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_space.disabled
|
||||
: is_error ? color_space.error
|
||||
: is_active ? color_space.focused
|
||||
: is_focused ? color_space.focused
|
||||
: color_space.enabled;
|
||||
}
|
||||
|
||||
auto filled_line_width() const -> double { return 1.5; }
|
||||
|
||||
static constexpr auto measure_text(
|
||||
const QFont& font, const QString& text, const QTextOption& options) {
|
||||
const auto fm = QFontMetricsF(font);
|
||||
const auto size = fm.size(Qt::TextSingleLine, text);
|
||||
return size.width();
|
||||
}
|
||||
|
||||
private:
|
||||
Measurements measurements;
|
||||
ColorSpace color_space;
|
||||
|
||||
bool is_disable = false;
|
||||
bool is_hovered = false;
|
||||
bool is_focused = false;
|
||||
bool is_error = false;
|
||||
bool is_active = false;
|
||||
|
||||
bool is_update_component_status = false;
|
||||
|
||||
QString label_text;
|
||||
QIcon leading_icon;
|
||||
QString leading_icon_code = material::icon::kArrowDropDown;
|
||||
QFont leading_icon_font = material::round::font_1;
|
||||
|
||||
QFont standard_text_font;
|
||||
|
||||
Animatable animatable;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> label_position;
|
||||
|
||||
DropdownMenu& self;
|
||||
};
|
||||
32
creeper-qt/widget/image.cc
Normal file
32
creeper-qt/widget/image.cc
Normal file
@@ -0,0 +1,32 @@
|
||||
#include "image.impl.hh"
|
||||
|
||||
Image::Image()
|
||||
: pimpl { std::make_unique<Impl>(*this) } { }
|
||||
|
||||
Image::~Image() = default;
|
||||
|
||||
auto Image::update_pixmap() noexcept -> void {
|
||||
pimpl->request_regenerate = true;
|
||||
this->update();
|
||||
}
|
||||
|
||||
auto Image::set_content_scale(ContentScale scale) noexcept -> void {
|
||||
pimpl->content_scale = scale;
|
||||
pimpl->request_regenerate = true;
|
||||
}
|
||||
auto Image::content_scale() const noexcept -> ContentScale { return pimpl->content_scale; }
|
||||
|
||||
auto Image::set_painter_resource(std::shared_ptr<PainterResource> resource) noexcept -> void {
|
||||
pimpl->resource_origin = std::move(resource);
|
||||
pimpl->resource_origin->add_finished_callback([this](auto&) { update(); });
|
||||
pimpl->request_regenerate = true;
|
||||
}
|
||||
auto Image::painter_resource() const noexcept -> PainterResource { return *pimpl->resource_origin; }
|
||||
|
||||
auto Image::set_opacity(double opacity) noexcept -> void { pimpl->opacity = opacity; }
|
||||
auto Image::set_radius(double radius) noexcept -> void { pimpl->radius = radius; }
|
||||
auto Image::set_border_width(double width) noexcept -> void { pimpl->border_width = width; }
|
||||
auto Image::set_border_color(QColor color) noexcept -> void { pimpl->border_color = color; }
|
||||
|
||||
auto Image::paintEvent(QPaintEvent* event) -> void { pimpl->paint_event(*event); }
|
||||
auto Image::resizeEvent(QResizeEvent* event) -> void { pimpl->resize_event(*event); }
|
||||
82
creeper-qt/widget/image.hh
Normal file
82
creeper-qt/widget/image.hh
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
#include "creeper-qt/utility/content-scale.hh"
|
||||
#include "creeper-qt/utility/painter-resource.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
namespace creeper {
|
||||
namespace image::internal {
|
||||
class Image : public QWidget {
|
||||
CREEPER_PIMPL_DEFINITION(Image)
|
||||
|
||||
public:
|
||||
auto update_pixmap() noexcept -> void;
|
||||
|
||||
auto set_content_scale(ContentScale) noexcept -> void;
|
||||
auto content_scale() const noexcept -> ContentScale;
|
||||
|
||||
auto set_painter_resource(std::shared_ptr<PainterResource>) noexcept -> void;
|
||||
auto painter_resource() const noexcept -> PainterResource;
|
||||
|
||||
auto set_opacity(double) noexcept -> void;
|
||||
auto set_radius(double) noexcept -> void;
|
||||
auto set_border_width(double) noexcept -> void;
|
||||
auto set_border_color(QColor) noexcept -> void;
|
||||
|
||||
protected:
|
||||
auto paintEvent(QPaintEvent*) -> void override;
|
||||
auto resizeEvent(QResizeEvent*) -> void override;
|
||||
};
|
||||
}
|
||||
namespace image::pro {
|
||||
|
||||
using Token = common::Token<internal::Image>;
|
||||
|
||||
struct ContentScale : Token {
|
||||
using T = creeper::ContentScale;
|
||||
T content_scale;
|
||||
explicit ContentScale(T content_scale) noexcept
|
||||
: content_scale { content_scale } { }
|
||||
explicit ContentScale(const auto& e) noexcept
|
||||
requires std::constructible_from<T, decltype(e)>
|
||||
: content_scale { e } { }
|
||||
auto apply(auto& self) const noexcept -> void
|
||||
requires requires { self.set_content_scale(content_scale); }
|
||||
{
|
||||
self.set_content_scale(content_scale);
|
||||
}
|
||||
};
|
||||
struct PainterResource : Token {
|
||||
using T = creeper::PainterResource;
|
||||
mutable std::shared_ptr<T> resource;
|
||||
|
||||
explicit PainterResource(std::shared_ptr<T> resource) noexcept
|
||||
: resource { std::move(resource) } { }
|
||||
|
||||
explicit PainterResource(auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: resource { std::make_shared<T>(std::forward<decltype(args)>(args)...) } { }
|
||||
|
||||
auto apply(auto& self) const noexcept -> void
|
||||
requires requires { self.set_painter_resource(std::move(resource)); }
|
||||
{
|
||||
self.set_painter_resource(std::move(resource));
|
||||
}
|
||||
};
|
||||
using Pixmap = PainterResource;
|
||||
|
||||
using Opacity = common::pro::Opacity<Token>;
|
||||
using Radius = common::pro::Radius<Token>;
|
||||
using BorderColor = common::pro::BorderColor<Token>;
|
||||
using BorderWidth = common::pro::BorderWidth<Token>;
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
}
|
||||
using Image =
|
||||
Declarative<image::internal::Image, CheckerOr<image::pro::checker, widget::pro::checker>>;
|
||||
}
|
||||
83
creeper-qt/widget/image.impl.hh
Normal file
83
creeper-qt/widget/image.impl.hh
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
#include "image.hh"
|
||||
|
||||
#include <qevent.h>
|
||||
#include <qpainter.h>
|
||||
#include <qpainterpath.h>
|
||||
#include <qpicture.h>
|
||||
#include <qwidget.h>
|
||||
|
||||
using namespace creeper::image::internal;
|
||||
|
||||
struct Image::Impl final {
|
||||
public:
|
||||
explicit Impl(Image& self) noexcept
|
||||
: self { self } { }
|
||||
|
||||
auto paint_event(const QPaintEvent&) noexcept {
|
||||
const auto radius = get_radius();
|
||||
const auto width = self.width();
|
||||
const auto height = self.height();
|
||||
|
||||
if (request_regenerate) regenerate_pixmap();
|
||||
|
||||
auto painter = QPainter { &self };
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
painter.setOpacity(opacity);
|
||||
|
||||
if (resource_render && !resource_render->isNull()) {
|
||||
const auto& pixmap = *resource_render;
|
||||
const auto& point = QPointF {
|
||||
(width - pixmap.width()) / 2.,
|
||||
(height - pixmap.height()) / 2.,
|
||||
};
|
||||
auto path = QPainterPath {};
|
||||
path.addRoundedRect(border_width, border_width, width - 2 * border_width,
|
||||
height - 2 * border_width, radius - border_width, radius - border_width);
|
||||
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setClipPath(path);
|
||||
painter.setClipping(true);
|
||||
painter.drawPixmap(point, pixmap);
|
||||
}
|
||||
{
|
||||
painter.setClipping(false);
|
||||
util::PainterHelper { painter }.rounded_rectangle(
|
||||
Qt::transparent, border_color, border_width, self.rect(), radius, radius);
|
||||
}
|
||||
}
|
||||
auto resize_event(const QResizeEvent&) noexcept { request_regenerate = true; }
|
||||
|
||||
private:
|
||||
auto regenerate_pixmap() noexcept -> void {
|
||||
if (!resource_origin) return;
|
||||
if (resource_origin->isNull()) return;
|
||||
const auto& _1 = *resource_origin;
|
||||
const auto& _2 = self.size();
|
||||
*resource_render = content_scale.transform(_1, _2);
|
||||
request_regenerate = false;
|
||||
}
|
||||
|
||||
auto get_radius() const noexcept -> double {
|
||||
return radius < 0 ? std::min<double>(self.width(), self.height()) / 2. : radius;
|
||||
}
|
||||
|
||||
public:
|
||||
ContentScale content_scale;
|
||||
bool request_regenerate = true;
|
||||
|
||||
double border_width = 02.;
|
||||
QColor border_color = Qt::transparent;
|
||||
|
||||
double radius = 10.;
|
||||
double opacity = 01.;
|
||||
|
||||
std::shared_ptr<PainterResource> resource_origin {};
|
||||
std::shared_ptr<PainterResource> resource_render {
|
||||
std::make_shared<PainterResource>(QPixmap {}),
|
||||
};
|
||||
|
||||
Image& self;
|
||||
};
|
||||
25
creeper-qt/widget/indicator/circular-progress-indicator.hh
Normal file
25
creeper-qt/widget/indicator/circular-progress-indicator.hh
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/widget/widget.hh"
|
||||
|
||||
namespace creeper::circular_progress_indicator::internal {
|
||||
|
||||
class CircularProgressIndicator : public Widget {
|
||||
CREEPER_PIMPL_DEFINITION(CircularProgressIndicator);
|
||||
|
||||
public:
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::circular_progress_indicator::pro {
|
||||
|
||||
using Token = common::Token<internal::CircularProgressIndicator>;
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
}
|
||||
namespace creeper { }
|
||||
12
creeper-qt/widget/main-window.cc
Normal file
12
creeper-qt/widget/main-window.cc
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "main-window.hh"
|
||||
|
||||
using namespace creeper::main_window::internal;
|
||||
|
||||
struct MainWindow::Impl { };
|
||||
|
||||
auto MainWindow::paintEvent(QPaintEvent* e) -> void { QMainWindow::paintEvent(e); }
|
||||
|
||||
MainWindow::MainWindow()
|
||||
: pimpl { std::make_unique<Impl>() } { }
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
79
creeper-qt/widget/main-window.hh
Normal file
79
creeper-qt/widget/main-window.hh
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
#include <qmainwindow.h>
|
||||
|
||||
#include "creeper-qt/utility/trait/widget.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
namespace creeper::main_window::internal {
|
||||
|
||||
template <class T>
|
||||
concept central_widget_trait = requires(T t, QWidget* widget) {
|
||||
{ t.setCentralWidget(widget) };
|
||||
};
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
CREEPER_PIMPL_DEFINITION(MainWindow)
|
||||
|
||||
protected:
|
||||
auto paintEvent(QPaintEvent*) -> void override;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::main_window::pro {
|
||||
using Token = common::Token<QMainWindow>;
|
||||
|
||||
template <widget_trait T>
|
||||
struct Central : Token {
|
||||
T* widget_pointer;
|
||||
|
||||
explicit Central(T* pointer) noexcept
|
||||
: widget_pointer { pointer } { }
|
||||
|
||||
explicit Central(auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: widget_pointer {
|
||||
new T { std::forward<decltype(args)>(args)... },
|
||||
} { }
|
||||
auto apply(internal::central_widget_trait auto& self) const noexcept -> void {
|
||||
self.setCentralWidget(this->widget_pointer);
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using MainWindow = Declarative<main_window::internal::MainWindow,
|
||||
CheckerOr<main_window::pro::checker, widget::pro::checker>>;
|
||||
|
||||
/// @brief 一点显示窗口的语法糖
|
||||
template <widget_trait T>
|
||||
struct ShowWindow final {
|
||||
T* window_pointer;
|
||||
explicit ShowWindow(auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: window_pointer {
|
||||
new T { std::forward<decltype(args)>(args)... },
|
||||
} {
|
||||
window_pointer->show();
|
||||
}
|
||||
explicit ShowWindow(T*& window, auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: ShowWindow { std::forward<decltype(args)>(args)... } {
|
||||
window = window_pointer;
|
||||
}
|
||||
explicit ShowWindow(std::invocable<T&> auto f, auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: ShowWindow { std::forward<decltype(args)>(args)... } {
|
||||
std::invoke(f, *window_pointer);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
42
creeper-qt/widget/shape/ellipse.hh
Normal file
42
creeper-qt/widget/shape/ellipse.hh
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/widget/shape/shape.hh"
|
||||
#include "creeper-qt/widget/widget.hh"
|
||||
|
||||
namespace creeper {
|
||||
|
||||
namespace ellipse::internal {
|
||||
class Ellipse : public Shape {
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override {
|
||||
auto painter = QPainter { this };
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
util::PainterHelper { painter }.ellipse(
|
||||
background_, border_color_, border_width_, rect());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
namespace ellipse::pro {
|
||||
using Token = common::Token<internal::Ellipse>;
|
||||
|
||||
// 通用属性
|
||||
using Background = common::pro::Background<Token>;
|
||||
|
||||
using BorderWidth = common::pro::BorderWidth<Token>;
|
||||
using BorderColor = common::pro::BorderColor<Token>;
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait)
|
||||
using namespace widget::pro;
|
||||
}
|
||||
|
||||
using Ellipse =
|
||||
Declarative<ellipse::internal::Ellipse, CheckerOr<ellipse::pro::checker, widget::pro::checker>>;
|
||||
|
||||
}
|
||||
101
creeper-qt/widget/shape/rounded-rect.hh
Normal file
101
creeper-qt/widget/shape/rounded-rect.hh
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
#include "creeper-qt/widget/shape/shape.hh"
|
||||
|
||||
namespace creeper::rounded_rect::internal {
|
||||
|
||||
class RoundedRect : public Shape {
|
||||
public:
|
||||
void set_radius(double radius) {
|
||||
radius_nx_ny_ = radius;
|
||||
radius_px_py_ = radius;
|
||||
radius_nx_py_ = radius;
|
||||
radius_px_ny_ = radius;
|
||||
update();
|
||||
}
|
||||
|
||||
void set_radius_nx_ny(double radius) {
|
||||
radius_nx_ny_ = radius;
|
||||
update();
|
||||
}
|
||||
void set_radius_px_py(double radius) {
|
||||
radius_px_py_ = radius;
|
||||
update();
|
||||
}
|
||||
void set_radius_nx_py(double radius) {
|
||||
radius_nx_py_ = radius;
|
||||
update();
|
||||
}
|
||||
void set_radius_px_ny(double radius) {
|
||||
radius_px_ny_ = radius;
|
||||
update();
|
||||
}
|
||||
|
||||
void set_radius_top_left(double radius) { set_radius_nx_ny(radius); }
|
||||
|
||||
void set_radius_top_right(double radius) { set_radius_px_ny(radius); }
|
||||
|
||||
void set_radius_bottom_left(double radius) { set_radius_nx_py(radius); }
|
||||
|
||||
void set_radius_bottom_right(double radius) { set_radius_px_py(radius); }
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override {
|
||||
auto painter = QPainter { this };
|
||||
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle( //
|
||||
background_, border_color_, border_width_, rect(),
|
||||
radius_nx_ny_, // tl: 左上
|
||||
radius_px_ny_, // tr: 右上
|
||||
radius_px_py_, // br: 右下
|
||||
radius_nx_py_ // bl: 左下
|
||||
)
|
||||
.done();
|
||||
}
|
||||
|
||||
private:
|
||||
double radius_nx_ny_ = 0;
|
||||
double radius_px_py_ = 0;
|
||||
double radius_nx_py_ = 0;
|
||||
double radius_px_ny_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::rounded_rect::pro {
|
||||
using Token = common::Token<internal::RoundedRect>;
|
||||
|
||||
// 通用属性
|
||||
using Radius = common::pro::Radius<Token>;
|
||||
|
||||
using RadiusPxPy = common::pro::RadiusPxPy<Token>;
|
||||
using RadiusNxNy = common::pro::RadiusNxNy<Token>;
|
||||
using RadiusPxNy = common::pro::RadiusPxNy<Token>;
|
||||
using RadiusNxPy = common::pro::RadiusNxPy<Token>;
|
||||
|
||||
using RadiusTopLeft = RadiusNxNy;
|
||||
using RadiusTopRight = RadiusPxNy;
|
||||
using RadiusBottomLeft = RadiusNxPy;
|
||||
using RadiusBottomRight = RadiusPxPy;
|
||||
|
||||
using Background = common::pro::Background<Token>;
|
||||
|
||||
using BorderWidth = common::pro::BorderWidth<Token>;
|
||||
using BorderColor = common::pro::BorderColor<Token>;
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait)
|
||||
using namespace widget::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using RoundedRect = Declarative<rounded_rect::internal::RoundedRect,
|
||||
CheckerOr<rounded_rect::pro::checker, widget::pro::checker>>;
|
||||
|
||||
}
|
||||
23
creeper-qt/widget/shape/shape.hh
Normal file
23
creeper-qt/widget/shape/shape.hh
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <qpainter.h>
|
||||
#include <qwidget.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
class Shape : public QWidget {
|
||||
public:
|
||||
using QWidget::QWidget;
|
||||
|
||||
void set_background(const QColor& color) { background_ = color; }
|
||||
|
||||
void set_border_color(const QColor& color) { border_color_ = color; }
|
||||
void set_border_width(double width) { border_width_ = width; }
|
||||
|
||||
protected:
|
||||
QColor background_ = Qt::gray;
|
||||
QColor border_color_ = Qt::black;
|
||||
double border_width_ = 0.;
|
||||
};
|
||||
|
||||
}
|
||||
128
creeper-qt/widget/shape/wave-circle.hh
Normal file
128
creeper-qt/widget/shape/wave-circle.hh
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/solution/round-angle.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/widget/shape/shape.hh"
|
||||
#include "creeper-qt/widget/widget.hh"
|
||||
|
||||
#include <cmath>
|
||||
#include <qpainterpath.h>
|
||||
#include <ranges>
|
||||
|
||||
namespace creeper::wave_circle::internal {
|
||||
|
||||
class WaveCircle : public Shape {
|
||||
public:
|
||||
auto set_flange_number(uint8_t number) noexcept {
|
||||
generate_request_ = true;
|
||||
flange_number_ = number;
|
||||
}
|
||||
auto set_flange_radius(double radius) noexcept {
|
||||
generate_request_ = true;
|
||||
flange_radius_ = radius;
|
||||
}
|
||||
auto set_overall_radius(double radius) noexcept {
|
||||
generate_request_ = true;
|
||||
overall_radius_ = radius;
|
||||
}
|
||||
auto set_protruding_ratio(double ratio) noexcept {
|
||||
generate_request_ = true;
|
||||
protruding_ratio_ = ratio;
|
||||
}
|
||||
|
||||
protected:
|
||||
auto paintEvent(QPaintEvent*) -> void override {
|
||||
if (generate_request_) generate_path();
|
||||
|
||||
auto painter = QPainter { this };
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setOpacity(1);
|
||||
painter.setBrush({ background_ });
|
||||
painter.setPen(QPen {
|
||||
border_color_,
|
||||
border_width_,
|
||||
Qt::SolidLine,
|
||||
Qt::RoundCap,
|
||||
});
|
||||
painter.drawPath(path_cache_);
|
||||
}
|
||||
auto resizeEvent(QResizeEvent* e) -> void override {
|
||||
Shape::resizeEvent(e);
|
||||
generate_request_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool generate_request_ = true;
|
||||
QPainterPath path_cache_;
|
||||
|
||||
int8_t flange_number_ = 12;
|
||||
double flange_radius_ = 10;
|
||||
double overall_radius_ = 100;
|
||||
double protruding_ratio_ = 0.8;
|
||||
|
||||
auto generate_path() noexcept -> void {
|
||||
|
||||
const auto center = QPointF(width() / 2., height() / 2.);
|
||||
const auto step = 2 * std::numbers::pi / flange_number_;
|
||||
const auto radius = std::min(overall_radius_, std::min<double>(width(), height()));
|
||||
|
||||
std::vector<QPointF> outside(flange_number_ + 2), inside(flange_number_ + 2);
|
||||
for (auto&& [index, point] : std::views::enumerate(std::views::zip(outside, inside))) {
|
||||
auto& [outside, inside] = point;
|
||||
outside.setX(radius * std::cos(-index * step));
|
||||
outside.setY(radius * std::sin(-index * step));
|
||||
inside.setX(protruding_ratio_ * radius * std::cos(double(-index + 0.5) * step));
|
||||
inside.setY(protruding_ratio_ * radius * std::sin(double(-index + 0.5) * step));
|
||||
}
|
||||
|
||||
auto begin = QPointF {};
|
||||
path_cache_ = QPainterPath {};
|
||||
for (int index = 0; index < flange_number_; index++) {
|
||||
const auto convex = RoundAngleSolution(center + outside[index], center + inside[index],
|
||||
center + inside[index + 1], flange_radius_);
|
||||
const auto concave = RoundAngleSolution(center + inside[index + 1],
|
||||
center + outside[index + 1], center + outside[index], flange_radius_);
|
||||
if (index == 0) begin = convex.start, path_cache_.moveTo(begin);
|
||||
path_cache_.lineTo(convex.start);
|
||||
path_cache_.arcTo(convex.rect, convex.angle_begin, convex.angle_length);
|
||||
path_cache_.lineTo(concave.end);
|
||||
path_cache_.arcTo(
|
||||
concave.rect, concave.angle_begin + concave.angle_length, -concave.angle_length);
|
||||
}
|
||||
path_cache_.lineTo(begin);
|
||||
}
|
||||
};
|
||||
}
|
||||
namespace creeper::wave_circle::pro {
|
||||
|
||||
using Token = common::Token<internal::WaveCircle>;
|
||||
|
||||
using Background = common::pro::Background<Token>;
|
||||
using BorderWidth = common::pro::BorderWidth<Token>;
|
||||
using BorderColor = common::pro::BorderColor<Token>;
|
||||
|
||||
using FlangeNumber =
|
||||
SetterProp<Token, uint8_t, [](auto& self, const auto& v) { self.set_flange_number(v); }>;
|
||||
|
||||
using FlangeRadius =
|
||||
SetterProp<Token, double, [](auto& self, const auto& v) { self.set_flange_radius(v); }>;
|
||||
|
||||
using OverallRadius =
|
||||
SetterProp<Token, double, [](auto& self, const auto& v) { self.set_overall_radius(v); }>;
|
||||
|
||||
using ProtrudingRatio =
|
||||
SetterProp<Token, double, [](auto& self, const auto& v) { self.set_protruding_ratio(v); }>;
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using WaveCircle = Declarative<wave_circle::internal::WaveCircle,
|
||||
CheckerOr<wave_circle::pro::checker, widget::pro::checker>>;
|
||||
|
||||
}
|
||||
37
creeper-qt/widget/sliders.cc
Normal file
37
creeper-qt/widget/sliders.cc
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "sliders.impl.hh"
|
||||
|
||||
Slider::Slider()
|
||||
: pimpl { std::make_unique<Impl>(*this) } { }
|
||||
|
||||
Slider::~Slider() = default;
|
||||
|
||||
auto Slider::set_color_scheme(const ColorScheme& scheme) -> void {
|
||||
pimpl->set_color_scheme(scheme);
|
||||
}
|
||||
auto Slider::set_measurements(const Measurements& measurements) -> void {
|
||||
pimpl->set_measurements(measurements);
|
||||
}
|
||||
auto Slider::load_theme_manager(ThemeManager& manager) -> void {
|
||||
pimpl->load_theme_manager(manager);
|
||||
}
|
||||
|
||||
auto Slider::set_progress(double progress) noexcept -> void {
|
||||
pimpl->set_progress(progress); //
|
||||
}
|
||||
auto Slider::get_progress() const noexcept -> double {
|
||||
return pimpl->get_progress(); //
|
||||
}
|
||||
|
||||
auto Slider::mousePressEvent(QMouseEvent* event) -> void {
|
||||
pimpl->mouse_press_event(event);
|
||||
QWidget::mousePressEvent(event);
|
||||
}
|
||||
auto Slider::mouseReleaseEvent(QMouseEvent* event) -> void {
|
||||
pimpl->mouse_release_event(event);
|
||||
QWidget::mouseReleaseEvent(event);
|
||||
}
|
||||
auto Slider::mouseMoveEvent(QMouseEvent* event) -> void {
|
||||
pimpl->mouse_move_event(event);
|
||||
QWidget::mouseMoveEvent(event);
|
||||
}
|
||||
auto Slider::paintEvent(QPaintEvent* event) -> void { pimpl->paint_event(event); }
|
||||
141
creeper-qt/widget/sliders.hh
Normal file
141
creeper-qt/widget/sliders.hh
Normal file
@@ -0,0 +1,141 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
namespace creeper::slider::internal {
|
||||
|
||||
class Slider : public QWidget {
|
||||
Q_OBJECT
|
||||
CREEPER_PIMPL_DEFINITION(Slider)
|
||||
|
||||
public:
|
||||
struct ColorSpecs {
|
||||
struct Tokens {
|
||||
QColor value_indicator = Qt::black;
|
||||
QColor value_text = Qt::white;
|
||||
|
||||
QColor stop_indicator_active = Qt::white;
|
||||
QColor stop_indicator_inactive = Qt::black;
|
||||
|
||||
QColor track_active = Qt::black;
|
||||
QColor track_inactive = Qt::gray;
|
||||
|
||||
QColor handle = Qt::black;
|
||||
};
|
||||
Tokens enabled;
|
||||
Tokens disabled;
|
||||
};
|
||||
|
||||
struct Measurements {
|
||||
int track_height = 16;
|
||||
|
||||
int label_container_height = 44;
|
||||
int label_container_width = 48;
|
||||
|
||||
int handle_height = 44;
|
||||
int handle_width = 4;
|
||||
|
||||
int track_shape = 8;
|
||||
|
||||
int inset_icon_size = 0;
|
||||
|
||||
constexpr auto minimum_height() const { return handle_height; }
|
||||
|
||||
static constexpr auto Xs() {
|
||||
return Measurements {
|
||||
.track_height = 16,
|
||||
.handle_height = 44,
|
||||
.track_shape = 8,
|
||||
.inset_icon_size = 0,
|
||||
};
|
||||
}
|
||||
static constexpr auto S() {
|
||||
return Measurements {
|
||||
.track_height = 24,
|
||||
.handle_height = 44,
|
||||
.track_shape = 8,
|
||||
.inset_icon_size = 0,
|
||||
};
|
||||
}
|
||||
static constexpr auto M() {
|
||||
return Measurements {
|
||||
.track_height = 40,
|
||||
.handle_height = 52,
|
||||
.track_shape = 12,
|
||||
.inset_icon_size = 24,
|
||||
};
|
||||
}
|
||||
static constexpr auto L() {
|
||||
return Measurements {
|
||||
.track_height = 56,
|
||||
.handle_height = 68,
|
||||
.track_shape = 16,
|
||||
.inset_icon_size = 24,
|
||||
};
|
||||
}
|
||||
static constexpr auto SL() {
|
||||
return Measurements {
|
||||
.track_height = 96,
|
||||
.handle_height = 108,
|
||||
.track_shape = 28,
|
||||
.inset_icon_size = 32,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
auto set_color_scheme(const ColorScheme&) -> void;
|
||||
auto set_measurements(const Measurements&) -> void;
|
||||
|
||||
auto load_theme_manager(ThemeManager&) -> void;
|
||||
|
||||
auto set_progress(double) noexcept -> void;
|
||||
auto get_progress() const noexcept -> double;
|
||||
|
||||
/// @bug Signals can not be exported on Windows
|
||||
signals:
|
||||
auto signal_value_change(double) -> void;
|
||||
auto signal_value_change_finished(double) -> void;
|
||||
|
||||
protected:
|
||||
auto mousePressEvent(QMouseEvent*) -> void override;
|
||||
auto mouseReleaseEvent(QMouseEvent*) -> void override;
|
||||
auto mouseMoveEvent(QMouseEvent*) -> void override;
|
||||
|
||||
auto paintEvent(QPaintEvent*) -> void override;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::slider::pro {
|
||||
|
||||
using Token = common::Token<internal::Slider>;
|
||||
|
||||
template <typename F>
|
||||
using OnValueChange =
|
||||
common::pro::SignalInjection<F, Token, &internal::Slider::signal_value_change>;
|
||||
|
||||
template <typename F>
|
||||
using OnValueChangeFinished =
|
||||
common::pro::SignalInjection<F, Token, &internal::Slider::signal_value_change_finished>;
|
||||
|
||||
using Measurements = SetterProp<Token, internal::Slider::Measurements,
|
||||
[](auto& self, const auto& v) { self.set_measurements(v); }>;
|
||||
|
||||
using Progress = SetterProp<Token, double, [](auto& self, auto v) { self.set_progress(v); }>;
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait)
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using Slider = Declarative<slider::internal::Slider,
|
||||
CheckerOr<slider::pro::checker, widget::pro::checker, theme::pro::checker>>;
|
||||
|
||||
}
|
||||
254
creeper-qt/widget/sliders.impl.hh
Normal file
254
creeper-qt/widget/sliders.impl.hh
Normal file
@@ -0,0 +1,254 @@
|
||||
#include "sliders.hh"
|
||||
|
||||
#include "creeper-qt/utility/animation/animatable.hh"
|
||||
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||
#include "creeper-qt/utility/animation/transition.hh"
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
|
||||
#include <qevent.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qpainter.h>
|
||||
|
||||
/// TODO:
|
||||
/// [ ] Adapt other directions
|
||||
/// [ ] Add Disable status
|
||||
/// [ ] Add Inset icon
|
||||
/// [ ] Add Stops
|
||||
/// [ ] Add Value indicator
|
||||
|
||||
using namespace creeper::slider::internal;
|
||||
|
||||
struct Slider::Impl {
|
||||
public:
|
||||
explicit Impl(Slider& self) noexcept
|
||||
: self { self }
|
||||
, animatable { self } {
|
||||
|
||||
// Transition For Handle Position
|
||||
{
|
||||
auto state = std::make_shared<PidState<double>>();
|
||||
|
||||
state->config.kp = 20.0;
|
||||
state->config.epsilon = 1e-4;
|
||||
|
||||
position = make_transition(animatable, std::move(state));
|
||||
}
|
||||
}
|
||||
|
||||
auto set_direction(Qt::ArrowType direction) noexcept -> void {
|
||||
this->direction = direction;
|
||||
self.update();
|
||||
}
|
||||
auto set_color_specs(const ColorSpecs& color_specs) noexcept -> void {
|
||||
this->color_specs = color_specs;
|
||||
self.update();
|
||||
}
|
||||
auto set_measurements(const Measurements& measurements) noexcept -> void {
|
||||
this->measurements = measurements;
|
||||
self.update();
|
||||
}
|
||||
|
||||
void set_color_scheme(const ColorScheme& scheme) {
|
||||
|
||||
// Alpha 97 (约 38%): 用于禁用状态的文本、图标等前景元素 (Text/Icon).
|
||||
auto on_surface_disabled_foreground = scheme.on_surface;
|
||||
on_surface_disabled_foreground.setAlpha(97);
|
||||
|
||||
// Alpha 31 (约 12%): 用于禁用状态的轨道、填充、容器等背景元素 (Container/Track).
|
||||
auto on_surface_disabled_container = scheme.on_surface;
|
||||
on_surface_disabled_container.setAlpha(31);
|
||||
|
||||
// --- 启用 (Enabled) 状态映射 ---
|
||||
auto& enabled = color_specs.enabled;
|
||||
|
||||
// 1. 值指示器 (气泡)
|
||||
enabled.value_indicator = scheme.inverse_surface;
|
||||
enabled.value_text = scheme.inverse_on_surface;
|
||||
|
||||
// 2. 停止点指示器 (Stop Indicators)
|
||||
enabled.stop_indicator_active = scheme.primary;
|
||||
enabled.stop_indicator_inactive = scheme.secondary_container;
|
||||
|
||||
// 3. 轨道 (Track)
|
||||
enabled.track_active = scheme.primary;
|
||||
enabled.track_inactive = scheme.secondary_container;
|
||||
|
||||
// 4. 拖动把手 (Handle)
|
||||
enabled.handle = scheme.primary;
|
||||
|
||||
// --- 禁用 (Disabled) 状态映射 ---
|
||||
auto& disabled = color_specs.disabled;
|
||||
|
||||
// 1. 值指示器 (气泡) - 气泡本身被禁用时,其背景和文字也应是禁用色
|
||||
disabled.value_indicator = on_surface_disabled_container;
|
||||
disabled.value_text = on_surface_disabled_foreground;
|
||||
|
||||
// 2. 停止点指示器
|
||||
disabled.stop_indicator_active = on_surface_disabled_container;
|
||||
disabled.stop_indicator_inactive = on_surface_disabled_container;
|
||||
|
||||
// 3. 轨道
|
||||
disabled.track_active = on_surface_disabled_container;
|
||||
disabled.track_inactive = on_surface_disabled_container;
|
||||
|
||||
// 4. 拖动把手
|
||||
disabled.handle = on_surface_disabled_container;
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& manager) { manager.append_handler(&self, self); }
|
||||
|
||||
auto set_progress(double progress, bool animatable = true) noexcept {
|
||||
this->progress = std::clamp(progress, 0.0, 1.0);
|
||||
if (animatable) {
|
||||
this->position->transition_to(progress);
|
||||
} else {
|
||||
this->position->snap_to(progress), self.update();
|
||||
}
|
||||
}
|
||||
|
||||
auto get_progress() const noexcept { return progress; }
|
||||
|
||||
public:
|
||||
auto paint_event(QPaintEvent*) -> void {
|
||||
|
||||
const auto& color = enabled ? color_specs.enabled : color_specs.disabled;
|
||||
|
||||
// TODO: Develop some util to simplify those calculating
|
||||
const auto handle_spacing = double { 1.5 * measurements.handle_width };
|
||||
const auto common_radius = double { 0.5 * measurements.handle_width };
|
||||
|
||||
// Handle shape
|
||||
const auto handle_color = color.handle;
|
||||
const auto handle_thickness = measurements.handle_width;
|
||||
const auto handle_length = measurements.handle_height;
|
||||
const auto handle_radius = 0.5 * handle_thickness;
|
||||
const auto handle_groove = self.width() - 2 * handle_thickness;
|
||||
const auto handle_center = is_horizontal()
|
||||
? QPointF { handle_thickness + *position * handle_groove, 0.5 * self.height() }
|
||||
: QPointF { 0.5 * self.width(), handle_thickness + *position * handle_groove };
|
||||
|
||||
const auto handle_thickness_real = pressed ? 0.5 * handle_thickness : handle_thickness;
|
||||
|
||||
const auto handle_w = is_horizontal() ? handle_thickness_real : handle_length;
|
||||
const auto handle_h = is_horizontal() ? handle_length : handle_thickness_real;
|
||||
|
||||
const auto handle_rectangle = QRectF {
|
||||
handle_center.x() - 0.5 * handle_w,
|
||||
handle_center.y() - 0.5 * handle_h,
|
||||
handle_w,
|
||||
handle_h,
|
||||
};
|
||||
|
||||
// Outline center of 4 sides
|
||||
const auto center_l = QPointF { 0.0 * self.width(), 0.5 * self.height() };
|
||||
const auto center_r = QPointF { 1.0 * self.width(), 0.5 * self.height() };
|
||||
const auto center_t = QPointF { 0.5 * self.width(), 0.0 * self.height() };
|
||||
const auto center_b = QPointF { 0.5 * self.width(), 1.0 * self.height() };
|
||||
|
||||
// Track shape
|
||||
const auto half_h = measurements.track_height / 2.;
|
||||
const auto track_1 = is_horizontal()
|
||||
? QRectF { center_l + QPointF { 0, -half_h },
|
||||
handle_center + QPointF { -handle_spacing, +half_h } }
|
||||
: QRectF { center_t + QPointF { -half_h, 0 },
|
||||
handle_center + QPointF { +half_h, -handle_spacing } };
|
||||
const auto track_2 = is_horizontal()
|
||||
? QRectF { handle_center + QPointF { +handle_spacing, -half_h },
|
||||
center_r + QPointF { 0, +half_h } }
|
||||
: QRectF { handle_center + QPointF { -half_h, +handle_spacing },
|
||||
center_b + QPointF { +half_h, 0 } };
|
||||
|
||||
const auto track_color_1 = color.track_active;
|
||||
const auto track_color_2 = color.track_inactive;
|
||||
const auto track_shape = measurements.track_shape;
|
||||
|
||||
// Stop Indicator
|
||||
|
||||
auto painter = QPainter { &self };
|
||||
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
|
||||
// Track Part 1
|
||||
.rounded_rectangle(track_color_1, Qt::transparent, 0, track_1, track_shape,
|
||||
common_radius, common_radius, track_shape)
|
||||
|
||||
// Track Part 2
|
||||
.rounded_rectangle(track_color_2, Qt::transparent, 0, track_2, common_radius,
|
||||
track_shape, track_shape, common_radius)
|
||||
|
||||
// Stop Indicator
|
||||
// TODO:
|
||||
|
||||
// Handle Shape
|
||||
.rounded_rectangle(
|
||||
handle_color, Qt::transparent, 0, handle_rectangle, handle_radius, handle_radius)
|
||||
|
||||
// Done
|
||||
.done();
|
||||
}
|
||||
auto mouse_release_event(QMouseEvent* event) noexcept -> void {
|
||||
if (!enabled) return;
|
||||
|
||||
pressed = false;
|
||||
update_progress(event->pos());
|
||||
emit self.signal_value_change_finished(progress);
|
||||
}
|
||||
auto mouse_press_event(QMouseEvent* event) noexcept -> void {
|
||||
if (!enabled) return;
|
||||
|
||||
pressed = true;
|
||||
update_progress(event->pos());
|
||||
emit self.signal_value_change(progress);
|
||||
}
|
||||
auto mouse_move_event(QMouseEvent* event) noexcept -> void {
|
||||
if (!enabled) return;
|
||||
|
||||
update_progress(event->pos());
|
||||
emit self.signal_value_change(progress);
|
||||
}
|
||||
|
||||
private:
|
||||
auto is_horizontal() const noexcept -> bool {
|
||||
return direction == Qt::RightArrow || direction == Qt::LeftArrow;
|
||||
}
|
||||
auto update_progress(const QPoint& point) noexcept -> void {
|
||||
const auto w = self.width();
|
||||
const auto h = self.height();
|
||||
const auto x = point.x();
|
||||
const auto y = point.y();
|
||||
|
||||
auto spindle_len = int {};
|
||||
auto spindle_pos = int {};
|
||||
|
||||
const auto thickness = measurements.handle_width;
|
||||
if (!is_horizontal()) {
|
||||
spindle_len = h - 2 * thickness;
|
||||
spindle_pos = y - 1 * thickness;
|
||||
} else {
|
||||
spindle_len = w - 2 * thickness;
|
||||
spindle_pos = x - 1 * thickness;
|
||||
}
|
||||
|
||||
progress = static_cast<double>(spindle_pos) / spindle_len;
|
||||
progress = std::clamp(progress, 0., 1.);
|
||||
|
||||
position->transition_to(progress);
|
||||
}
|
||||
|
||||
private:
|
||||
double progress = 0.0;
|
||||
uint steps = 0;
|
||||
bool enabled = true;
|
||||
bool pressed = false;
|
||||
|
||||
Qt::ArrowType direction = Qt::RightArrow;
|
||||
|
||||
ColorSpecs color_specs = ColorSpecs {};
|
||||
Measurements measurements = Measurements::Xs();
|
||||
|
||||
Animatable animatable;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> position;
|
||||
|
||||
Slider& self;
|
||||
};
|
||||
63
creeper-qt/widget/switch.cc
Normal file
63
creeper-qt/widget/switch.cc
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "switch.impl.hh"
|
||||
|
||||
Switch::Switch()
|
||||
: pimpl(std::make_unique<Impl>(*this)) { }
|
||||
|
||||
Switch::~Switch() = default;
|
||||
|
||||
void Switch::set_color_scheme(const ColorScheme& scheme) {
|
||||
pimpl->set_color_scheme(*this, scheme), update();
|
||||
}
|
||||
|
||||
void Switch::load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(
|
||||
this, [this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
|
||||
void Switch::set_disabled(bool on) { pimpl->set_disabled(*this, on); }
|
||||
bool Switch::disabled() const { return pimpl->disabled; }
|
||||
|
||||
void Switch::set_checked(bool on) { pimpl->set_checked(*this, on); }
|
||||
bool Switch::checked() const { return pimpl->checked; }
|
||||
|
||||
void Switch::set_track_color_unchecked(const QColor& color) { pimpl->track_unchecked = color; }
|
||||
void Switch::set_track_color_checked(const QColor& color) { pimpl->track_checked = color; }
|
||||
void Switch::set_track_color_unchecked_disabled(const QColor& color) {
|
||||
pimpl->track_unchecked_disabled = color;
|
||||
}
|
||||
void Switch::set_track_color_checked_disabled(const QColor& color) {
|
||||
pimpl->track_checked_disabled = color;
|
||||
}
|
||||
|
||||
void Switch::set_handle_color_unchecked(const QColor& color) { pimpl->handle_unchecked = color; }
|
||||
void Switch::set_handle_color_checked(const QColor& color) { pimpl->handle_checked = color; }
|
||||
void Switch::set_handle_color_unchecked_disabled(const QColor& color) {
|
||||
pimpl->handle_unchecked_disabled = color;
|
||||
}
|
||||
void Switch::set_handle_color_checked_disabled(const QColor& color) {
|
||||
pimpl->handle_checked_disabled = color;
|
||||
}
|
||||
|
||||
void Switch::set_outline_color_unchecked(const QColor& color) { pimpl->outline_unchecked = color; }
|
||||
void Switch::set_outline_color_checked(const QColor& color) { pimpl->outline_checked = color; }
|
||||
void Switch::set_outline_color_unchecked_disabled(const QColor& color) {
|
||||
pimpl->outline_unchecked_disabled = color;
|
||||
}
|
||||
void Switch::set_outline_color_checked_disabled(const QColor& color) {
|
||||
pimpl->outline_checked_disabled = color;
|
||||
}
|
||||
|
||||
void Switch::set_hover_color_unchecked(const QColor& color) { pimpl->hover_unchecked = color; }
|
||||
void Switch::set_hover_color_checked(const QColor& color) { pimpl->hover_checked = color; }
|
||||
|
||||
void Switch::enterEvent(qt::EnterEvent* event) {
|
||||
pimpl->enter_event(*this, *event);
|
||||
QAbstractButton::enterEvent(event);
|
||||
}
|
||||
|
||||
void Switch::leaveEvent(QEvent* event) {
|
||||
pimpl->leave_event(*this, *event);
|
||||
QAbstractButton::leaveEvent(event);
|
||||
}
|
||||
|
||||
void Switch::paintEvent(QPaintEvent* event) { pimpl->paint_event(*this, *event); }
|
||||
119
creeper-qt/widget/switch.hh
Normal file
119
creeper-qt/widget/switch.hh
Normal file
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/pimpl.hh"
|
||||
#include "creeper-qt/utility/wrapper/property.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
#include <qabstractbutton.h>
|
||||
|
||||
namespace creeper {
|
||||
namespace _switch::internal {
|
||||
class Switch : public QAbstractButton {
|
||||
CREEPER_PIMPL_DEFINITION(Switch)
|
||||
|
||||
public:
|
||||
void set_color_scheme(const ColorScheme&);
|
||||
void load_theme_manager(ThemeManager&);
|
||||
|
||||
void set_disabled(bool);
|
||||
bool disabled() const;
|
||||
|
||||
void set_checked(bool);
|
||||
bool checked() const;
|
||||
|
||||
void set_track_color_unchecked(const QColor&);
|
||||
void set_track_color_checked(const QColor&);
|
||||
void set_track_color_unchecked_disabled(const QColor&);
|
||||
void set_track_color_checked_disabled(const QColor&);
|
||||
|
||||
void set_handle_color_unchecked(const QColor&);
|
||||
void set_handle_color_checked(const QColor&);
|
||||
void set_handle_color_unchecked_disabled(const QColor&);
|
||||
void set_handle_color_checked_disabled(const QColor&);
|
||||
|
||||
void set_outline_color_unchecked(const QColor&);
|
||||
void set_outline_color_checked(const QColor&);
|
||||
void set_outline_color_unchecked_disabled(const QColor&);
|
||||
void set_outline_color_checked_disabled(const QColor&);
|
||||
|
||||
void set_hover_color_unchecked(const QColor&);
|
||||
void set_hover_color_checked(const QColor&);
|
||||
|
||||
protected:
|
||||
// 添加 Hover 动画
|
||||
void enterEvent(qt::EnterEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
// 实现视觉效果
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
};
|
||||
}
|
||||
namespace _switch::pro {
|
||||
|
||||
using Token = common::Token<internal::Switch>;
|
||||
|
||||
/// @note 碎碎念,这么多颜色,真的会用得上么...
|
||||
|
||||
using TrackColorUnchecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_track_color_unchecked(v); }>;
|
||||
|
||||
using TrackColorChecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_track_color_checked(v); }>;
|
||||
|
||||
using TrackColorUncheckedDisabled = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_track_color_unchecked_disabled(v); }>;
|
||||
|
||||
using TrackColorCheckedDisabled = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_track_color_checked_disabled(v); }>;
|
||||
|
||||
using HandleColorUnchecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_handle_color_unchecked(v); }>;
|
||||
|
||||
using HandleColorChecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_handle_color_checked(v); }>;
|
||||
|
||||
using HandleColorUncheckedDisabled = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_handle_color_unchecked_disabled(v); }>;
|
||||
|
||||
using HandleColorCheckedDisabled = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_handle_color_checked_disabled(v); }>;
|
||||
|
||||
using OutlineColorUnchecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_outline_color_unchecked(v); }>;
|
||||
|
||||
using OutlineColorChecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_outline_color_checked(v); }>;
|
||||
|
||||
using OutlineColorUncheckedDisabled = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_outline_color_unchecked_disabled(v); }>;
|
||||
|
||||
using OutlineColorCheckedDisabled = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_outline_color_checked_disabled(v); }>;
|
||||
|
||||
using HoverColorUnchecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_hover_color_unchecked(v); }>;
|
||||
|
||||
using HoverColorChecked = SetterProp<Token, QColor,
|
||||
[](auto& self, const QColor& v) { self.set_hover_color_checked(v); }>;
|
||||
|
||||
template <typename Callback>
|
||||
using Clickable = common::pro::Clickable<Callback, Token>;
|
||||
|
||||
using Disabled = common::pro::Disabled<Token>;
|
||||
using Checked = common::pro::Checked<Token>;
|
||||
|
||||
template <class Switch>
|
||||
concept trait = std::derived_from<Switch, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace theme::pro;
|
||||
using namespace widget::pro;
|
||||
}
|
||||
/// @note 使用时建议比例 w : h > 7 : 4 ,过冲动画会多占用一些宽度,倘若 w 过短,可能会出现 hover
|
||||
/// 层画面被截断的情况
|
||||
using Switch = Declarative<_switch::internal::Switch,
|
||||
CheckerOr<_switch::pro::checker, widget::pro::checker, theme::pro::checker>>;
|
||||
}
|
||||
204
creeper-qt/widget/switch.impl.hh
Normal file
204
creeper-qt/widget/switch.impl.hh
Normal file
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
#include "switch.hh"
|
||||
|
||||
#include <qpainter.h>
|
||||
|
||||
#include "creeper-qt/utility/animation/animatable.hh"
|
||||
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||
#include "creeper-qt/utility/animation/state/spring.hh"
|
||||
#include "creeper-qt/utility/animation/transition.hh"
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
|
||||
using namespace creeper::_switch::internal;
|
||||
|
||||
struct Switch::Impl {
|
||||
|
||||
bool checked = false;
|
||||
bool disabled = false;
|
||||
bool hovered = false;
|
||||
|
||||
QColor track_unchecked;
|
||||
QColor track_checked;
|
||||
QColor track_unchecked_disabled;
|
||||
QColor track_checked_disabled;
|
||||
|
||||
QColor handle_unchecked;
|
||||
QColor handle_checked;
|
||||
QColor handle_unchecked_disabled;
|
||||
QColor handle_checked_disabled;
|
||||
|
||||
QColor outline_unchecked;
|
||||
QColor outline_checked;
|
||||
QColor outline_unchecked_disabled;
|
||||
QColor outline_checked_disabled;
|
||||
|
||||
QColor hover_unchecked;
|
||||
QColor hover_checked;
|
||||
|
||||
static constexpr double position_unchecked = 0.0;
|
||||
static constexpr double position_checked = 1.0;
|
||||
|
||||
Animatable animatable;
|
||||
|
||||
std::unique_ptr<TransitionValue<PidState<Eigen::Vector4d>>> track;
|
||||
std::unique_ptr<TransitionValue<PidState<Eigen::Vector4d>>> handle;
|
||||
std::unique_ptr<TransitionValue<PidState<Eigen::Vector4d>>> outline;
|
||||
|
||||
std::unique_ptr<TransitionValue<SpringState<double>>> position;
|
||||
|
||||
explicit Impl(Switch& self) noexcept
|
||||
: animatable(self) {
|
||||
|
||||
// @TODO: 适配进 MotionScheme
|
||||
constexpr double kp = 15.0, ki = 0.0, kd = 0.0, hz = 90;
|
||||
constexpr double k = 400, d = 22;
|
||||
{
|
||||
constexpr auto make_state = [] {
|
||||
auto state = std::make_shared<PidState<Eigen::Vector4d>>();
|
||||
|
||||
state->config.kp = kp;
|
||||
state->config.ki = ki;
|
||||
state->config.kd = kd;
|
||||
|
||||
return state;
|
||||
};
|
||||
track = make_transition(animatable, make_state());
|
||||
handle = make_transition(animatable, make_state());
|
||||
outline = make_transition(animatable, make_state());
|
||||
}
|
||||
{
|
||||
auto state = std::make_shared<SpringState<double>>();
|
||||
|
||||
state->config.k = k;
|
||||
state->config.d = d;
|
||||
|
||||
position = make_transition(animatable, std::move(state));
|
||||
}
|
||||
|
||||
QObject::connect(&self, &Switch::clicked, [this, &self] { set_checked(self, !checked); });
|
||||
}
|
||||
|
||||
void set_color_scheme(Switch& self, const ColorScheme& scheme) {
|
||||
track_unchecked = scheme.surface_variant;
|
||||
track_checked = scheme.primary;
|
||||
track_unchecked_disabled = scheme.surface_variant;
|
||||
track_checked_disabled = scheme.on_surface;
|
||||
|
||||
handle_unchecked = scheme.outline;
|
||||
handle_checked = scheme.on_primary;
|
||||
handle_unchecked_disabled = scheme.on_surface;
|
||||
handle_checked_disabled = scheme.surface;
|
||||
|
||||
outline_unchecked = scheme.outline;
|
||||
outline_checked = scheme.primary;
|
||||
outline_unchecked_disabled = scheme.on_surface;
|
||||
outline_checked_disabled = Qt::transparent;
|
||||
|
||||
hover_checked = scheme.primary;
|
||||
hover_unchecked = scheme.on_surface;
|
||||
|
||||
constexpr auto disabled_track_opacity = 0.12;
|
||||
track_unchecked_disabled.setAlphaF(disabled_track_opacity);
|
||||
track_checked_disabled.setAlphaF(disabled_track_opacity);
|
||||
outline_unchecked_disabled.setAlphaF(disabled_track_opacity);
|
||||
|
||||
constexpr auto disabled_handle_opacity = 0.38;
|
||||
handle_unchecked_disabled.setAlphaF(disabled_handle_opacity);
|
||||
handle_checked_disabled.setAlphaF(disabled_handle_opacity);
|
||||
|
||||
constexpr auto hover_opacity = 0.08;
|
||||
hover_checked.setAlphaF(hover_opacity);
|
||||
hover_unchecked.setAlphaF(hover_opacity);
|
||||
|
||||
update_switch_ui(self, checked, disabled);
|
||||
}
|
||||
|
||||
void set_disabled(Switch& self, bool on) {
|
||||
if (disabled == on) return;
|
||||
|
||||
disabled = on;
|
||||
update_switch_ui(self, checked, on);
|
||||
}
|
||||
|
||||
void set_checked(Switch& self, bool on) {
|
||||
if (disabled || checked == on) return;
|
||||
|
||||
checked = on;
|
||||
update_switch_ui(self, on);
|
||||
}
|
||||
|
||||
void enter_event(Switch& self, const QEvent& event) {
|
||||
if (!disabled) self.setCursor(Qt::PointingHandCursor);
|
||||
hovered = true;
|
||||
}
|
||||
|
||||
void leave_event(Switch& self, const QEvent& event) { hovered = false; }
|
||||
|
||||
void paint_event(Switch& self, const QPaintEvent& event) {
|
||||
const auto rect = extract_rect(self.rect(), 13, 8);
|
||||
|
||||
// 外轮廓相关变量计算
|
||||
const auto hover_radius = std::min<double>(rect.width(), rect.height()) / 2;
|
||||
|
||||
const auto outline_radius = hover_radius * 4 / 5;
|
||||
const auto outline_width = outline_radius / 8;
|
||||
const auto outline_error = hover_radius - outline_radius;
|
||||
const auto outline_rect = rect.adjusted( //
|
||||
outline_error, outline_error, -outline_error, -outline_error);
|
||||
|
||||
// 计算 handle 半径
|
||||
const auto handle_radius_checked = outline_radius / 4 * 3;
|
||||
const auto handle_radius_unchecked = outline_radius / 2 * 1;
|
||||
|
||||
const auto handle_radius =
|
||||
handle_radius_unchecked + *position * (handle_radius_checked - handle_radius_unchecked);
|
||||
|
||||
// 计算 handle 坐标
|
||||
const auto handle_point_begin = rect.topLeft() + QPointF { hover_radius, hover_radius };
|
||||
const auto handle_point_end =
|
||||
rect.topLeft() + QPointF { rect.width() - hover_radius, rect.height() - hover_radius };
|
||||
|
||||
const auto handle_point =
|
||||
handle_point_begin + *position * (handle_point_end - handle_point_begin);
|
||||
|
||||
// 选择 hover 颜色
|
||||
const auto hover_color =
|
||||
(!disabled && hovered) ? (checked ? hover_checked : hover_unchecked) : Qt::transparent;
|
||||
|
||||
auto painter = QPainter { &self };
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::RenderHint::Antialiasing)
|
||||
.rounded_rectangle(from_vector4(*track), from_vector4(*outline), outline_width,
|
||||
outline_rect, outline_radius, outline_radius)
|
||||
.ellipse(from_vector4(*handle), Qt::transparent, 0, handle_point, handle_radius,
|
||||
handle_radius)
|
||||
.ellipse(hover_color, Qt::transparent, 0, handle_point, hover_radius, hover_radius)
|
||||
.done();
|
||||
}
|
||||
|
||||
private:
|
||||
auto update_switch_ui(Switch&, bool checked, bool disabled = false) -> void {
|
||||
|
||||
// 添加 Switch 轨道颜色动画
|
||||
const auto track_target = disabled
|
||||
? (checked ? track_checked_disabled : track_unchecked_disabled)
|
||||
: (checked ? track_checked : track_unchecked);
|
||||
track->transition_to(from_color(track_target));
|
||||
|
||||
// 添加 Switch 指示器颜色动画
|
||||
const auto handle_target = disabled
|
||||
? (checked ? handle_checked_disabled : handle_unchecked_disabled)
|
||||
: (checked ? handle_checked : handle_unchecked);
|
||||
handle->transition_to(from_color(handle_target));
|
||||
|
||||
// 添加 Switch 外边框颜色动画
|
||||
const auto outline_target = disabled
|
||||
? (checked ? outline_checked_disabled : outline_unchecked_disabled)
|
||||
: (checked ? outline_checked : outline_unchecked);
|
||||
outline->transition_to(from_color(outline_target));
|
||||
|
||||
// 添加 Switch 运动动画
|
||||
const auto position_target = checked ? position_checked : position_unchecked;
|
||||
position->transition_to(position_target);
|
||||
}
|
||||
};
|
||||
73
creeper-qt/widget/text-fields.cc
Normal file
73
creeper-qt/widget/text-fields.cc
Normal file
@@ -0,0 +1,73 @@
|
||||
#include "text-fields.impl.hh"
|
||||
|
||||
BasicTextField::BasicTextField()
|
||||
: pimpl { std::make_unique<Impl>(*this) } { }
|
||||
|
||||
BasicTextField::~BasicTextField() = default;
|
||||
|
||||
auto BasicTextField::set_color_scheme(const ColorScheme& scheme) -> void {
|
||||
pimpl->set_color_scheme(scheme);
|
||||
}
|
||||
|
||||
auto BasicTextField::load_theme_manager(ThemeManager& manager) -> void {
|
||||
pimpl->load_theme_manager(manager);
|
||||
}
|
||||
|
||||
auto BasicTextField::set_label_text(const QString& text) -> void {
|
||||
pimpl->set_label_text(text); //
|
||||
}
|
||||
|
||||
auto BasicTextField::set_hint_text(const QString& text) -> void { }
|
||||
|
||||
auto BasicTextField::set_supporting_text(const QString& text) -> void { }
|
||||
|
||||
auto BasicTextField::set_leading_icon(const QIcon& text) -> void { }
|
||||
|
||||
auto BasicTextField::set_leading_icon(const QString& code, const QString& font) -> void {
|
||||
pimpl->set_leading_icon(code, font);
|
||||
}
|
||||
|
||||
auto BasicTextField::set_trailling_icon(const QIcon& text) -> void { }
|
||||
|
||||
auto BasicTextField::set_trailling_icon(const QString& code, const QString& font) -> void { }
|
||||
|
||||
auto BasicTextField::set_measurements(const Measurements& measurements) noexcept -> void {
|
||||
pimpl->set_measurements(measurements);
|
||||
}
|
||||
|
||||
auto BasicTextField::resizeEvent(QResizeEvent* event) -> void {
|
||||
//
|
||||
QLineEdit::resizeEvent(event);
|
||||
}
|
||||
|
||||
auto BasicTextField::enterEvent(qt::EnterEvent* event) -> void {
|
||||
pimpl->enter_event(event);
|
||||
QLineEdit::enterEvent(event);
|
||||
}
|
||||
|
||||
auto BasicTextField::leaveEvent(QEvent* event) -> void {
|
||||
pimpl->leave_event(event);
|
||||
QLineEdit::leaveEvent(event);
|
||||
}
|
||||
|
||||
auto BasicTextField::focusInEvent(QFocusEvent* event) -> void {
|
||||
pimpl->focus_in(event);
|
||||
QLineEdit::focusInEvent(event);
|
||||
}
|
||||
|
||||
auto BasicTextField::focusOutEvent(QFocusEvent* event) -> void {
|
||||
pimpl->focus_out(event);
|
||||
QLineEdit::focusOutEvent(event);
|
||||
}
|
||||
|
||||
using namespace creeper;
|
||||
|
||||
auto FilledTextField::paintEvent(QPaintEvent* event) -> void {
|
||||
pimpl->paint_filled(event);
|
||||
QLineEdit::paintEvent(event);
|
||||
}
|
||||
|
||||
auto OutlinedTextField::paintEvent(QPaintEvent* event) -> void {
|
||||
pimpl->paint_outlined(event);
|
||||
QLineEdit::paintEvent(event);
|
||||
}
|
||||
150
creeper-qt/widget/text-fields.hh
Normal file
150
creeper-qt/widget/text-fields.hh
Normal file
@@ -0,0 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/qt_wrapper/enter-event.hh"
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
#include <qlineedit.h>
|
||||
|
||||
namespace creeper {
|
||||
class FilledTextField;
|
||||
class OutlinedTextField;
|
||||
|
||||
namespace text_field::internal {
|
||||
class BasicTextField : public QLineEdit {
|
||||
CREEPER_PIMPL_DEFINITION(BasicTextField);
|
||||
|
||||
friend FilledTextField;
|
||||
friend OutlinedTextField;
|
||||
|
||||
public:
|
||||
struct ColorSpecs {
|
||||
struct Tokens {
|
||||
QColor container;
|
||||
QColor caret;
|
||||
QColor active_indicator;
|
||||
|
||||
QColor input_text;
|
||||
QColor label_text;
|
||||
QColor supporting_text;
|
||||
|
||||
QColor leading_icon;
|
||||
QColor trailing_icon;
|
||||
|
||||
QColor outline;
|
||||
};
|
||||
|
||||
Tokens enabled;
|
||||
Tokens disabled;
|
||||
Tokens focused;
|
||||
Tokens error;
|
||||
|
||||
QColor state_layer;
|
||||
QColor selection_container;
|
||||
};
|
||||
|
||||
struct Measurements {
|
||||
int container_height = 56;
|
||||
|
||||
int icon_rect_size = 24;
|
||||
int input_rect_size = 24;
|
||||
int label_rect_size = 16;
|
||||
|
||||
int standard_font_height = 16;
|
||||
|
||||
int col_padding = 8;
|
||||
int row_padding_without_icons = 16;
|
||||
int row_padding_with_icons = 12;
|
||||
int row_padding_populated_label_text = 4;
|
||||
|
||||
int padding_icons_text = 16;
|
||||
|
||||
int supporting_text_and_character_counter_top_padding = 4;
|
||||
int supporting_text_and_character_counter_row_padding = 16;
|
||||
|
||||
auto icon_size() const { return QSize { icon_rect_size, icon_rect_size }; }
|
||||
};
|
||||
|
||||
void set_color_scheme(const ColorScheme&);
|
||||
|
||||
void load_theme_manager(ThemeManager&);
|
||||
|
||||
void set_label_text(const QString&);
|
||||
|
||||
void set_hint_text(const QString&);
|
||||
|
||||
void set_supporting_text(const QString&);
|
||||
|
||||
void set_leading_icon(const QIcon&);
|
||||
|
||||
void set_leading_icon(const QString& code, const QString& font);
|
||||
|
||||
void set_trailling_icon(const QIcon&);
|
||||
|
||||
void set_trailling_icon(const QString& code, const QString& font);
|
||||
|
||||
auto set_measurements(const Measurements& measurements) noexcept -> void;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent*) override;
|
||||
|
||||
void enterEvent(qt::EnterEvent*) override;
|
||||
void leaveEvent(QEvent*) override;
|
||||
|
||||
void focusInEvent(QFocusEvent*) override;
|
||||
void focusOutEvent(QFocusEvent*) override;
|
||||
};
|
||||
}
|
||||
namespace text_field::pro {
|
||||
using Token = common::Token<internal::BasicTextField>;
|
||||
|
||||
using ClearButton = SetterProp<Token, bool,
|
||||
[](auto& self, bool enable) { self.setClearButtonEnabled(enable); }>;
|
||||
|
||||
using LabelText = common::pro::String<Token,
|
||||
[](auto& self, const auto& string) { self.set_label_text(string); }>;
|
||||
|
||||
struct LeadingIcon : Token {
|
||||
QString code;
|
||||
QString font;
|
||||
explicit LeadingIcon(const QString& code, const QString& font)
|
||||
: code { code }
|
||||
, font { font } { }
|
||||
void apply(auto& self) const { self.set_leading_icon(code, font); }
|
||||
};
|
||||
|
||||
template <typename F>
|
||||
using OnTextChanged =
|
||||
common::pro::SignalInjection<F, Token, &internal::BasicTextField::textChanged>;
|
||||
|
||||
template <typename F>
|
||||
using OnEditingFinished =
|
||||
common::pro::SignalInjection<F, Token, &internal::BasicTextField::editingFinished>;
|
||||
|
||||
template <typename F>
|
||||
using OnChanged = OnTextChanged<F>;
|
||||
|
||||
template <class TextField>
|
||||
concept trait = std::derived_from<TextField, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
|
||||
struct FilledTextField
|
||||
: public Declarative<text_field::internal::BasicTextField,
|
||||
CheckerOr<text_field::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||
using Declarative::Declarative;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
};
|
||||
|
||||
struct OutlinedTextField
|
||||
: public Declarative<text_field::internal::BasicTextField,
|
||||
CheckerOr<text_field::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||
using Declarative::Declarative;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
};
|
||||
|
||||
}
|
||||
472
creeper-qt/widget/text-fields.impl.hh
Normal file
472
creeper-qt/widget/text-fields.impl.hh
Normal file
@@ -0,0 +1,472 @@
|
||||
#include "text-fields.hh"
|
||||
|
||||
#include "creeper-qt/utility/animation/animatable.hh"
|
||||
#include "creeper-qt/utility/animation/state/pid.hh"
|
||||
#include "creeper-qt/utility/animation/transition.hh"
|
||||
#include "creeper-qt/utility/painter/common.hh"
|
||||
#include "creeper-qt/utility/painter/container.hh"
|
||||
#include "creeper-qt/utility/painter/helper.hh"
|
||||
#include "creeper-qt/utility/painter/shape.hh"
|
||||
|
||||
#include <qpainter.h>
|
||||
#include <qpainterpath.h>
|
||||
|
||||
using namespace creeper::text_field::internal;
|
||||
|
||||
struct BasicTextField::Impl {
|
||||
public:
|
||||
explicit Impl(BasicTextField& 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));
|
||||
}
|
||||
|
||||
self.setAlignment(Qt::AlignVCenter);
|
||||
set_measurements(Measurements {});
|
||||
}
|
||||
|
||||
/// @note https://m3.material.io/components/text-fields/specs
|
||||
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.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.enabled.trailing_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.leading_icon = scheme.on_surface;
|
||||
color_specs.disabled.leading_icon.setAlphaF(0.38);
|
||||
color_specs.disabled.trailing_icon = scheme.on_surface;
|
||||
color_specs.disabled.trailing_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.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.focused.trailing_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.input_text = scheme.on_surface;
|
||||
color_specs.error.supporting_text = scheme.error;
|
||||
color_specs.error.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.error.trailing_icon = scheme.error;
|
||||
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, //
|
||||
Qt::transparent, //
|
||||
color.input_text, //
|
||||
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_font_name = font;
|
||||
|
||||
is_update_component_status = false;
|
||||
use_leading_icon = true;
|
||||
}
|
||||
|
||||
auto set_measurements(const Measurements& measurements) noexcept -> 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 widget_rect = self.rect();
|
||||
const auto color = get_color_tokens();
|
||||
|
||||
constexpr auto container_radius = 5;
|
||||
|
||||
update_component_status(FieldType::FILLED);
|
||||
|
||||
auto painter = QPainter { &self };
|
||||
|
||||
const auto container_rect = QRect { widget_rect.left(),
|
||||
widget_rect.top() + (widget_rect.height() - measurements.container_height) / 2,
|
||||
widget_rect.width(), measurements.container_height };
|
||||
|
||||
// Container
|
||||
{
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color.container, Qt::transparent, 0, container_rect,
|
||||
container_radius, container_radius, 0, 0);
|
||||
}
|
||||
|
||||
// Active Indicator
|
||||
{
|
||||
const auto p0 = container_rect.bottomLeft();
|
||||
const auto p1 = container_rect.bottomRight();
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen { color.active_indicator, filled_line_width() });
|
||||
painter.drawLine(p0, p1);
|
||||
}
|
||||
|
||||
// Leading icon
|
||||
if (!leading_icon.isNull()) {
|
||||
//
|
||||
} else if (!leading_icon_code.isEmpty()) {
|
||||
const auto icon_rect = QRectF {
|
||||
1.0 * container_rect.left() + measurements.row_padding_with_icons,
|
||||
1.0 * container_rect.top()
|
||||
+ (measurements.container_height - measurements.icon_rect_size) / 2.0,
|
||||
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);
|
||||
painter.drawText(icon_rect, leading_icon_code, { Qt::AlignCenter });
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
// Label Text
|
||||
if (!label_text.isEmpty()) {
|
||||
const auto position = self.text().isEmpty() ? *label_position : 1.;
|
||||
const auto margins = self.textMargins();
|
||||
|
||||
const auto center_label_y = container_rect.top()
|
||||
+ (measurements.container_height - measurements.label_rect_size) / 2.0;
|
||||
|
||||
const auto rect_center = QRectF {
|
||||
QPointF(margins.left(), center_label_y),
|
||||
QPointF(container_rect.right() - margins.right(),
|
||||
center_label_y + measurements.label_rect_size),
|
||||
};
|
||||
|
||||
const auto rect_top = QRectF {
|
||||
QPointF(margins.left(), container_rect.top() + measurements.col_padding),
|
||||
QPointF(container_rect.right() - margins.right(),
|
||||
container_rect.top() + measurements.col_padding + measurements.label_rect_size),
|
||||
};
|
||||
|
||||
const auto rect = animate::interpolate(rect_center, rect_top, position);
|
||||
const auto scale = 1. - position * 0.25;
|
||||
const auto anchor = QPointF { rect.left(), rect.center().y() };
|
||||
|
||||
painter.save();
|
||||
painter.translate(anchor);
|
||||
painter.scale(scale, scale);
|
||||
painter.translate(-anchor);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen { color.label_text });
|
||||
painter.setFont(standard_text_font);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.drawText(rect, label_text, { Qt::AlignVCenter | Qt::AlignLeading });
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
// Hovered State Layer
|
||||
if (is_hovered) {
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color_specs.state_layer, Qt::transparent, 0, container_rect,
|
||||
container_radius, container_radius, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
auto paint_outlined(QPaintEvent*) -> void {
|
||||
const auto& measurements = this->measurements;
|
||||
const auto& color_tokens = get_color_tokens();
|
||||
|
||||
update_component_status(FieldType::OUTLINED);
|
||||
{
|
||||
using namespace painter;
|
||||
using namespace painter::common::pro;
|
||||
auto painter = qt::painter { &self };
|
||||
|
||||
/// Cache Calculate
|
||||
const auto container_width = self.width();
|
||||
const auto container_height = measurements.container_height;
|
||||
|
||||
const auto input_leading_padding = use_leading_icon
|
||||
? measurements.row_padding_with_icons + measurements.icon_rect_size
|
||||
+ measurements.padding_icons_text
|
||||
: measurements.row_padding_without_icons;
|
||||
const auto input_trailing_padding = use_trailing_icon
|
||||
? measurements.row_padding_with_icons + measurements.icon_rect_size
|
||||
+ measurements.padding_icons_text
|
||||
: measurements.row_padding_without_icons;
|
||||
const auto input_vertical_padding =
|
||||
0.5 * (measurements.container_height - measurements.icon_rect_size);
|
||||
|
||||
/// Container
|
||||
const auto container_size = qt::size(self.width(), container_height);
|
||||
const auto container_thickness = is_focused ? 2. : is_hovered ? 1.5 : 1.;
|
||||
|
||||
/// Leading Icon
|
||||
const auto leading_box_size = use_leading_icon
|
||||
? qt::size(measurements.icon_rect_size, container_height)
|
||||
: qt::size(0, 0);
|
||||
|
||||
/// Label Text
|
||||
const auto position = self.text().isEmpty() ? *label_position : 1.;
|
||||
const auto text_scale = animate::interpolate(1., 0.75, position);
|
||||
|
||||
auto text_option = qt::text_option {};
|
||||
text_option.setWrapMode(QTextOption::NoWrap);
|
||||
text_option.setAlignment(Qt::AlignLeading | Qt::AlignVCenter);
|
||||
|
||||
const auto text_width = measure_text(standard_text_font, label_text, text_option);
|
||||
|
||||
auto label_origin = qt::point {};
|
||||
auto label_size = qt::size {};
|
||||
{
|
||||
const auto begin_y = input_vertical_padding;
|
||||
const auto final_y = -0.5 * measurements.standard_font_height;
|
||||
|
||||
const auto top_padding = use_leading_icon ? measurements.row_padding_with_icons
|
||||
: measurements.row_padding_without_icons;
|
||||
const auto begin_origin = qt::point(input_leading_padding, begin_y);
|
||||
const auto final_origin = qt::point(top_padding, final_y);
|
||||
|
||||
const auto begin_size = qt::size(text_width, measurements.icon_rect_size);
|
||||
const auto final_size =
|
||||
qt::size(text_scale * text_width, measurements.standard_font_height);
|
||||
|
||||
label_origin = animate::interpolate(begin_origin, final_origin, position);
|
||||
label_size = animate::interpolate(begin_size, final_size, position);
|
||||
}
|
||||
const auto label_background_size = qt::size {
|
||||
label_size.width() + 2 * measurements.row_padding_populated_label_text,
|
||||
label_size.height(),
|
||||
};
|
||||
|
||||
Paint::Box {
|
||||
BoxImpl { self.size(), Qt::AlignCenter },
|
||||
Paint::Surface {
|
||||
SurfaceImpl { container_size },
|
||||
Paint::Buffer {
|
||||
BufferImpl { container_size },
|
||||
Paint::RoundedRectangle {
|
||||
Size { container_size },
|
||||
Outline { color_tokens.outline, container_thickness },
|
||||
Radiuses { 5 },
|
||||
},
|
||||
Paint::EraseRectangle {
|
||||
Origin { label_origin },
|
||||
Size { label_background_size },
|
||||
},
|
||||
},
|
||||
Paint::Box {
|
||||
BoxImpl { label_background_size, Qt::AlignHCenter, label_origin },
|
||||
Paint::Text {
|
||||
TextOption { text_option },
|
||||
Font { standard_text_font },
|
||||
Size { label_background_size },
|
||||
Text { label_text },
|
||||
Color { color_tokens.label_text },
|
||||
Scale { text_scale },
|
||||
},
|
||||
},
|
||||
Paint::Box {
|
||||
BoxImpl { leading_box_size, Qt::AlignCenter,
|
||||
{ 1. * measurements.row_padding_with_icons, 0 } },
|
||||
Paint::Icon {
|
||||
Icon { leading_font_name, leading_icon_code },
|
||||
Size { leading_box_size },
|
||||
Color { color_tokens.leading_icon },
|
||||
},
|
||||
},
|
||||
},
|
||||
}(painter);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
auto focus_out(QFocusEvent*) -> void {
|
||||
is_focused = false;
|
||||
update_label_position();
|
||||
}
|
||||
|
||||
private:
|
||||
enum class FieldType { FILLED, OUTLINED };
|
||||
|
||||
auto update_component_status(FieldType type) -> void {
|
||||
if (is_update_component_status) return;
|
||||
|
||||
const auto padding_with_icon = measurements.row_padding_with_icons
|
||||
+ measurements.icon_rect_size + measurements.padding_icons_text;
|
||||
const auto padding_without_icon = measurements.row_padding_without_icons;
|
||||
|
||||
const auto left_padding = use_leading_icon ? padding_with_icon : padding_without_icon;
|
||||
const auto tail_padding = use_trailing_icon ? padding_with_icon : padding_without_icon;
|
||||
|
||||
switch (type) {
|
||||
case FieldType::FILLED: {
|
||||
const auto top_padding = measurements.col_padding + measurements.label_rect_size;
|
||||
const auto bot_padding = measurements.col_padding;
|
||||
self.setTextMargins(left_padding, top_padding, tail_padding, bot_padding);
|
||||
break;
|
||||
}
|
||||
case FieldType::OUTLINED: {
|
||||
const auto input_padding =
|
||||
0.5 * (measurements.container_height - measurements.input_rect_size);
|
||||
self.setTextMargins(left_padding, input_padding, tail_padding, input_padding);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
leading_icon_font = QFont { leading_font_name };
|
||||
leading_icon_font.setPointSizeF(
|
||||
0.5 * (measurements.standard_font_height + measurements.icon_rect_size));
|
||||
|
||||
is_update_component_status = true;
|
||||
}
|
||||
|
||||
auto update_label_position() -> void { label_position->transition_to(is_focused ? 1.0 : 0.0); }
|
||||
|
||||
auto sync_basic_text_style(const QColor& text, const QColor& background,
|
||||
const QColor& selection_text, const QColor& selection_background) -> 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 kQLineEditStyle = R"(
|
||||
QLineEdit {
|
||||
border: none;
|
||||
color: %1;
|
||||
background-color: %2;
|
||||
selection-color: %3;
|
||||
selection-background-color: %4;
|
||||
}
|
||||
)";
|
||||
|
||||
const auto qss = QString { kQLineEditStyle };
|
||||
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 -> ColorSpecs::Tokens const& {
|
||||
return is_disable ? color_specs.disabled
|
||||
: is_error ? color_specs.error
|
||||
: 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;
|
||||
ColorSpecs color_specs;
|
||||
|
||||
bool is_disable = false;
|
||||
bool is_hovered = false;
|
||||
bool is_focused = false;
|
||||
bool is_error = false;
|
||||
bool is_update_component_status = false;
|
||||
|
||||
QString label_text;
|
||||
QString hint_text;
|
||||
QString supporting_text;
|
||||
|
||||
bool use_leading_icon = false;
|
||||
bool use_trailing_icon = false;
|
||||
|
||||
QIcon leading_icon;
|
||||
QString leading_icon_code;
|
||||
QString leading_font_name;
|
||||
|
||||
QIcon trailing_icon;
|
||||
QString trailing_code;
|
||||
QString trailing_font;
|
||||
|
||||
// State Cache
|
||||
QFont leading_icon_font = {};
|
||||
QFont standard_text_font = {};
|
||||
|
||||
Animatable animatable;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> label_position;
|
||||
|
||||
BasicTextField& self;
|
||||
};
|
||||
60
creeper-qt/widget/text.hh
Normal file
60
creeper-qt/widget/text.hh
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "creeper-qt/utility/theme/theme.hh"
|
||||
#include "creeper-qt/utility/wrapper/common.hh"
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
#include <qlabel.h>
|
||||
|
||||
namespace creeper::text::internal {
|
||||
|
||||
class Text : public QLabel {
|
||||
using QLabel::QLabel;
|
||||
|
||||
public:
|
||||
auto set_color_scheme(const ColorScheme& scheme) noexcept -> void {
|
||||
set_color(scheme.on_surface);
|
||||
}
|
||||
auto load_theme_manager(ThemeManager& manager) noexcept -> void {
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
|
||||
auto set_color(QColor color) noexcept -> void {
|
||||
const auto name = color.name(QColor::HexArgb);
|
||||
const auto style = QString("QLabel { color : %1; }");
|
||||
setStyleSheet(style.arg(name));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::text::pro {
|
||||
|
||||
using Token = common::Token<internal::Text>;
|
||||
|
||||
using Text = common::pro::Text<Token>;
|
||||
|
||||
using Color = SetterProp<Token, QColor, [](auto& self, const auto& v) { self.set_color(v); }>;
|
||||
|
||||
using WordWrap = SetterProp<Token, bool, [](auto& self, const auto& v) { self.setWordWrap(v); }>;
|
||||
|
||||
using AdjustSize = ActionProp<Token, [](auto& self) { self.adjustSize(); }>;
|
||||
|
||||
using Alignment =
|
||||
SetterProp<Token, Qt::Alignment, [](auto& self, const auto& v) { self.setAlignment(v); }>;
|
||||
|
||||
using TextInteractionFlags = SetterProp<Token, Qt::TextInteractionFlags,
|
||||
[](auto& self, const auto& v) { self.setTextInteractionFlags(v); }>;
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using Text = Declarative<text::internal::Text,
|
||||
CheckerOr<text::pro::checker, widget::pro::checker, theme::pro::checker>>;
|
||||
|
||||
}
|
||||
3
creeper-qt/widget/widget.cc
Normal file
3
creeper-qt/widget/widget.cc
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "widget.hh"
|
||||
|
||||
using namespace creeper;
|
||||
11
creeper-qt/widget/widget.hh
Normal file
11
creeper-qt/widget/widget.hh
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "creeper-qt/utility/wrapper/widget.hh"
|
||||
|
||||
namespace creeper::widget::internal {
|
||||
|
||||
class Widget : public QWidget { };
|
||||
|
||||
}
|
||||
namespace creeper {
|
||||
using Widget = Declarative<widget::internal::Widget, widget::pro::checker>;
|
||||
}
|
||||
Reference in New Issue
Block a user