first commit
This commit is contained in:
88
CMakeLists.txt
Normal file
88
CMakeLists.txt
Normal file
@@ -0,0 +1,88 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(touchsensor VERSION 2.0.0 LANGUAGES CXX)
|
||||
|
||||
set(BUILD_EXAMPLE ON)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_BUILD_TYPE "Release")
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
add_compile_options(-Os -O3)
|
||||
|
||||
set(QT_VERSION Qt6)
|
||||
find_package(${QT_VERSION} REQUIRED COMPONENTS Widgets Network)
|
||||
find_package(Eigen3 REQUIRED)
|
||||
|
||||
# For #include "..."
|
||||
include_directories(.)
|
||||
|
||||
# 库文件收集
|
||||
file(
|
||||
GLOB_RECURSE PROJECT_SOURCE
|
||||
CONFIGURE_DEPENDS
|
||||
# Project source
|
||||
"modern-qt/*.cc"
|
||||
# Custom signals
|
||||
"modern-qt/widget/sliders.hh"
|
||||
)
|
||||
add_library(
|
||||
modern-qt SHARED
|
||||
${PROJECT_SOURCE}
|
||||
modern-qt/widget/select.hh
|
||||
modern-qt/widget/select.impl.hh
|
||||
)
|
||||
target_link_libraries(
|
||||
modern-qt PUBLIC
|
||||
${QT_VERSION}::Widgets
|
||||
${QT_VERSION}::Network
|
||||
)
|
||||
|
||||
file(
|
||||
GLOB_RECURSE WIDGETS_CC
|
||||
CONFIGURE_DEPENDS "components/*.cc"
|
||||
)
|
||||
|
||||
file(
|
||||
GLOB_RECURSE QCUSTOMPLOT_SOURCE
|
||||
CONFIGURE_DEPENDS
|
||||
"qcustomplot/*.cpp"
|
||||
"qcustomplot/*.h"
|
||||
)
|
||||
add_library(
|
||||
qcustomplot SHARED
|
||||
${QCUSTOMPLOT_SOURCE}
|
||||
)
|
||||
target_link_libraries(
|
||||
qcustomplot PUBLIC
|
||||
${QT_VERSION}::Core
|
||||
${QT_VERSION}::Gui
|
||||
)
|
||||
|
||||
qt_standard_project_setup()
|
||||
add_executable(
|
||||
${PROJECT_NAME}
|
||||
${WIDGETS_CC}
|
||||
main.cc
|
||||
component.hh
|
||||
resources.qrc
|
||||
components/view.cc
|
||||
modern-qt/widget/select.cc
|
||||
components/charts/heatmap.cc
|
||||
components/charts/heatmap.hh
|
||||
components/charts/heatmap.impl.hh
|
||||
)
|
||||
qt6_add_resources(QRC_FILES resources.qrc)
|
||||
target_sources(${PROJECT_NAME} PRIVATE ${QRC_FILES})
|
||||
#qt_add_resources(QRC_FILES resources.qrc)
|
||||
#qt_add_resources(${PROJECT_NAME} ${QRC_FILES} resources.qrc)
|
||||
#target_sources(${PROJECT_NAME} PRIVATE ${QRC_FILES})
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
${QT_VERSION}::Widgets
|
||||
${QT_VERSION}::Network
|
||||
modern-qt
|
||||
)
|
||||
8
cmake/creeper-qtConfig.cmake.in
Normal file
8
cmake/creeper-qtConfig.cmake.in
Normal file
@@ -0,0 +1,8 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
find_dependency(Qt5Widgets "@REQUIRED_QT_VERSION@")
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/creeper-qtTargets.cmake")
|
||||
|
||||
check_required_components(Spix)
|
||||
21
component.hh
Normal file
21
component.hh
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <modern-qt/utility/theme/theme.hh>
|
||||
#include <qwidget.h>
|
||||
|
||||
template <typename T>
|
||||
using raw_pointer = T*;
|
||||
|
||||
struct NavComponentState {
|
||||
creeper::ThemeManager& manager;
|
||||
std::function<void(int, const std::string_view&)> switch_callback;
|
||||
|
||||
std::vector<std::tuple<std::string_view, std::string_view>> buttons_context;
|
||||
};
|
||||
|
||||
auto NavComponent(NavComponentState&) noexcept -> raw_pointer<QWidget>;
|
||||
|
||||
struct ViewComponentState {
|
||||
creeper::ThemeManager& manager;
|
||||
};
|
||||
auto ViewComponent(ViewComponentState&) noexcept -> raw_pointer<QWidget>;
|
||||
12
components/charts/heatmap.cc
Normal file
12
components/charts/heatmap.cc
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#include "heatmap.hh"
|
||||
#include "heatmap.impl.hh"
|
||||
|
||||
BasicPlot::BasicPlot()
|
||||
: pimpl {std::make_unique<Impl>(*this)} {}
|
||||
|
||||
void BasicPlot::set_matrix_size(const QSize &) {
|
||||
}
|
||||
57
components/charts/heatmap.hh
Normal file
57
components/charts/heatmap.hh
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_H
|
||||
#define TOUCHSENSOR_HEATMAP_H
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "qcustomplot/qcustomplot.h"
|
||||
|
||||
namespace creeper {
|
||||
class heatmap;
|
||||
|
||||
namespace plot_widget::internal {
|
||||
class BasicPlot : public QCustomPlot {
|
||||
CREEPER_PIMPL_DEFINITION(BasicPlot);
|
||||
friend heatmap;
|
||||
|
||||
public:
|
||||
struct ColorSpace {
|
||||
|
||||
};
|
||||
|
||||
void load_theme_manager(ThemeManager&);
|
||||
|
||||
void set_xlabel_text(const QString&);
|
||||
|
||||
void set_ylabel_text(const QString&);
|
||||
|
||||
void set_matrix_size(const QSize&);
|
||||
|
||||
private:
|
||||
friend class Impl;
|
||||
};
|
||||
}
|
||||
|
||||
namespace plot_widget::pro {
|
||||
using Token = common::Token<internal::BasicPlot>;
|
||||
using XLabelText = common::pro::String<Token, [](auto& self, const auto& string) {
|
||||
self.set_xlabel_text(string);
|
||||
}>;
|
||||
using YLabelText = common::pro::String<Token, [](auto& self, const auto& string) {
|
||||
self.set_ylabel_text(string);
|
||||
}>;
|
||||
|
||||
struct MatrixSize : Token {
|
||||
QSize size;
|
||||
explicit MatrixSize(const int& w, const int& h) : size {w, h} {}
|
||||
void apply(auto& self) const {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif //TOUCHSENSOR_HEATMAP_H
|
||||
22
components/charts/heatmap.impl.hh
Normal file
22
components/charts/heatmap.impl.hh
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/17.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
#define TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
|
||||
#include "heatmap.hh"
|
||||
|
||||
using namespace creeper::plot_widget::internal;
|
||||
|
||||
struct BasicPlot::Impl {
|
||||
explicit Impl(BasicSelect& self) noexcept
|
||||
: self{self} {
|
||||
|
||||
}
|
||||
private:
|
||||
|
||||
BasicPlot& self;
|
||||
};
|
||||
|
||||
#endif //TOUCHSENSOR_HEATMAP_IMPL_HH
|
||||
103
components/nav.cc
Normal file
103
components/nav.cc
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "component.hh"
|
||||
|
||||
#include "modern-qt/core/application.hh"
|
||||
#include "modern-qt/layout/group.hh"
|
||||
#include "modern-qt/layout/linear.hh"
|
||||
#include "modern-qt/layout/mutual-exclusion-group.hh"
|
||||
#include "modern-qt/utility/material-icon.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/widget/buttons/icon-button.hh"
|
||||
#include "modern-qt/widget/cards/filled-card.hh"
|
||||
#include "modern-qt/widget/image.hh"
|
||||
|
||||
using namespace creeper;
|
||||
namespace fc = filled_card::pro;
|
||||
namespace sg = select_group::pro;
|
||||
namespace ln = linear::pro;
|
||||
namespace im = image::pro;
|
||||
namespace ic = icon_button::pro;
|
||||
|
||||
auto NavComponent(NavComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
|
||||
const auto AvatarComponent = new Image {
|
||||
im::FixedSize {60, 60},
|
||||
im::Radius {-1},
|
||||
im::ContentScale {ContentScale::CROP},
|
||||
im::BorderWidth {3},
|
||||
im::PainterResource {
|
||||
":/images/images/logo.png",
|
||||
// "./images/logo.png",
|
||||
},
|
||||
};
|
||||
state.manager.append_handler(AvatarComponent, [AvatarComponent](const ThemeManager& manager) {
|
||||
const auto colorscheme = manager.color_scheme();
|
||||
const auto colorborder = colorscheme.secondary_container;
|
||||
AvatarComponent->set_border_color(colorborder);
|
||||
});
|
||||
|
||||
const auto navigation_icons_config = std::tuple {
|
||||
ic::ThemeManager {state.manager},
|
||||
ic::ColorStandard,
|
||||
ic::ShapeRound,
|
||||
ic::TypesToggleUnselected,
|
||||
ic::WidthDefault,
|
||||
ic::Font {material::regular::font_1},
|
||||
ic::FixedSize {IconButton::kSmallContainerSize},
|
||||
};
|
||||
|
||||
return new FilledCard {
|
||||
fc::ThemeManager {state.manager},
|
||||
fc::Radius {0},
|
||||
fc::Level {CardLevel::HIGHEST},
|
||||
fc::Layout<Col> {
|
||||
ln::Spacing {10},
|
||||
ln::Margin {15},
|
||||
ln::Item {
|
||||
{0, Qt::AlignHCenter},
|
||||
AvatarComponent,
|
||||
},
|
||||
ln::SpacingItem {20},
|
||||
ln::Item<SelectGroup<Col, IconButton>> {
|
||||
{0, Qt::AlignHCenter},
|
||||
ln::Margin {0},
|
||||
ln::SpacingItem {10},
|
||||
sg::Compose {
|
||||
state.buttons_context | std::views::enumerate,
|
||||
[&](int index, const auto& context) {
|
||||
const auto& [name, icon] = context;
|
||||
|
||||
const auto status = (index == 0)
|
||||
? ic::TypesToggleSelected
|
||||
: ic::TypesToggleUnselected;
|
||||
|
||||
return new IconButton {
|
||||
navigation_icons_config,
|
||||
status,
|
||||
ic::FontIcon(icon.data()),
|
||||
ic::Clickable {[=]{state.switch_callback(index, name);}},
|
||||
};
|
||||
},
|
||||
Qt::AlignHCenter,
|
||||
},
|
||||
sg::SignalInjection{&IconButton::clicked},
|
||||
},
|
||||
ln::SpacingItem {40},
|
||||
ln::Stretch {255},
|
||||
ln::Item<IconButton> {
|
||||
{0, Qt::AlignHCenter},
|
||||
navigation_icons_config,
|
||||
ic::TypesDefault,
|
||||
ic::FontIcon {material::icon::kLogout},
|
||||
ic::Clickable {&app::quit},
|
||||
},
|
||||
ln::Item<IconButton> {
|
||||
{0, Qt::AlignHCenter},
|
||||
navigation_icons_config,
|
||||
ic::ColorFilled,
|
||||
ic::FontIcon {material::icon::kDarkMode},
|
||||
ic::Clickable{[&]{state.manager.toggle_color_mode();state.manager.apply_theme();}},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
178
components/view.cc
Normal file
178
components/view.cc
Normal file
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
#include <random>
|
||||
|
||||
#include "component.hh"
|
||||
|
||||
#include <modern-qt/layout/flow.hh>
|
||||
#include <modern-qt/layout/linear.hh>
|
||||
#include <modern-qt/utility/material-icon.hh>
|
||||
#include <modern-qt/utility/wrapper/mutable-value.hh>
|
||||
#include <modern-qt/widget/buttons/icon-button.hh>
|
||||
#include <modern-qt/widget/cards/filled-card.hh>
|
||||
#include <modern-qt/widget/cards/outlined-card.hh>
|
||||
#include <modern-qt/widget/image.hh>
|
||||
#include <modern-qt/widget/shape/wave-circle.hh>
|
||||
#include <modern-qt/widget/sliders.hh>
|
||||
#include <modern-qt/widget/switch.hh>
|
||||
#include <modern-qt/widget/text-fields.hh>
|
||||
#include <modern-qt/widget/text.hh>
|
||||
#include <modern-qt/widget/select.hh>
|
||||
|
||||
using namespace creeper;
|
||||
namespace capro = card::pro;
|
||||
namespace lnpro = linear::pro;
|
||||
namespace impro = image::pro;
|
||||
namespace ibpro = icon_button::pro;
|
||||
namespace slpro = select_widget::pro;
|
||||
|
||||
static auto ComConfigComponent(ThemeManager& manager, auto&& callback) {
|
||||
auto slogen_context = std::make_shared<MutableValue<QString>>();
|
||||
slogen_context->set_silent("BanG Bream! It's MyGo!!!");
|
||||
|
||||
// 创建一个MutableValue<QStringList>来存储MatSelect的选项
|
||||
auto select_com_context = std::make_shared<MutableValue<QStringList>>();
|
||||
select_com_context->set_silent(QStringList {});
|
||||
|
||||
auto select_baud_context = std::make_shared<MutableValue<QStringList>>();
|
||||
select_baud_context->set_silent(QStringList {"9600", "115200"});
|
||||
|
||||
const auto row = new Row {
|
||||
// lnpro::Item<FilledTextField> {
|
||||
// text_field::pro::ThemeManager {manager},
|
||||
// text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font},
|
||||
// MutableForward {
|
||||
// text_field::pro::LabelText {},
|
||||
// slogen_context,
|
||||
// },
|
||||
// },
|
||||
lnpro::Item<MatSelect> {
|
||||
slpro::ThemeManager {manager},
|
||||
slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
|
||||
slpro::LeadingText {"COM"},
|
||||
},
|
||||
lnpro::Item<MatSelect> {
|
||||
slpro::ThemeManager {manager },
|
||||
slpro::LeadingIcon { material::icon::kArrowDropDown, material::regular::font},
|
||||
slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}},
|
||||
slpro::LeadingText {"Baud"},
|
||||
// slpro::MutableItems {select_baud_context},
|
||||
MutableForward {
|
||||
slpro::SelectItems {},
|
||||
select_baud_context,
|
||||
}
|
||||
},
|
||||
// lnpro::Item<MatSelect> {
|
||||
// // slpro::ThemeManager {manager},
|
||||
// // slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font},
|
||||
// // slpro::IndexChanged {}
|
||||
// // }
|
||||
lnpro::SpacingItem {20},
|
||||
lnpro::Item<IconButton> {
|
||||
ibpro::ThemeManager {manager},
|
||||
ibpro::FixedSize {40, 40},
|
||||
ibpro::Color { IconButton::Color::TONAL },
|
||||
ibpro::Font { material::kRoundSmallFont },
|
||||
// ibpro::FontIcon { material::icon::kFavorite },
|
||||
ibpro::FontIcon { material::icon::kAddLink },
|
||||
ibpro::Clickable {[slogen_context] {
|
||||
constexpr auto random_slogen = [] {
|
||||
constexpr auto slogens = std::array {
|
||||
"为什么要演奏《春日影》!",
|
||||
"我从来不觉得玩乐队开心过。",
|
||||
"我好想…成为人啊!",
|
||||
"那你愿意……跟我组一辈子的乐队吗?",
|
||||
"过去软弱的我…已经死了。",
|
||||
};
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dist(0, slogens.size() - 1);
|
||||
return QString::fromUtf8(slogens[dist(gen)]);
|
||||
};
|
||||
*slogen_context = random_slogen();
|
||||
}},
|
||||
},
|
||||
lnpro::Item<IconButton> {
|
||||
ibpro::ThemeManager { manager },
|
||||
ibpro::FixedSize { 40, 40 },
|
||||
ibpro::Color { IconButton::Color::TONAL },
|
||||
ibpro::Font { material::kRoundSmallFont },
|
||||
ibpro::FontIcon { material::icon::kRefresh },
|
||||
ibpro::Clickable {[select_baud_context] {
|
||||
// 定义两组不同的选项
|
||||
static constexpr auto options_group1 = std::array {
|
||||
"第一组选项1", "第一组选项2", "第一组选项3"
|
||||
};
|
||||
static constexpr auto options_group2 = std::array {
|
||||
"第二组选项A", "第二组选项B", "第二组选项C", "第二组选项D"
|
||||
};
|
||||
|
||||
// 随机选择一组选项
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dist(0, 1);
|
||||
|
||||
QStringList new_options;
|
||||
if (dist(gen) == 0) {
|
||||
for (const auto& option : options_group1) {
|
||||
new_options << QString::fromUtf8(option);
|
||||
}
|
||||
} else {
|
||||
for (const auto& option : options_group2) {
|
||||
new_options << QString::fromUtf8(option);
|
||||
}
|
||||
}
|
||||
qDebug() << new_options;
|
||||
|
||||
// 更新选项列表,MatSelect会自动刷新
|
||||
*select_baud_context = new_options;
|
||||
}},
|
||||
},
|
||||
|
||||
};
|
||||
return new Widget {
|
||||
widget::pro::Layout {row},
|
||||
};
|
||||
}
|
||||
|
||||
auto ViewComponent(ViewComponentState& state) noexcept -> raw_pointer<QWidget> {
|
||||
const auto texts = std::array {
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
std::make_shared<MutableValue<QString>>("0.500"),
|
||||
};
|
||||
const auto progresses = std::array {
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
std::make_shared<MutableValue<double>>(0.5),
|
||||
};
|
||||
return new FilledCard {
|
||||
capro::ThemeManager { state.manager },
|
||||
capro::SizePolicy {QSizePolicy::Expanding},
|
||||
capro::Layout<Col> {
|
||||
lnpro::Alignment {Qt::AlignTop},
|
||||
lnpro::Margin {10},
|
||||
lnpro::Spacing {10},
|
||||
|
||||
lnpro::Item {
|
||||
ComConfigComponent(state.manager,
|
||||
[texts, progresses] {
|
||||
constexpr auto random_unit = []() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_real_distribution<double> dist(0.0, 1.0);
|
||||
return dist(gen);
|
||||
};
|
||||
for (auto&& [string, number] : std::views::zip(texts, progresses)) {
|
||||
auto v = random_unit();
|
||||
*number = v;
|
||||
*string = QString::number(v, 'f', 3);
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
BIN
images/logo.png
Normal file
BIN
images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 569 B |
87
main.cc
Normal file
87
main.cc
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "component.hh"
|
||||
#include <qstandardpaths.h>
|
||||
#include <modern-qt/core/application.hh>
|
||||
#include <modern-qt/layout/linear.hh>
|
||||
#include <modern-qt/layout/mixer.hh>
|
||||
#include <modern-qt/layout/scroll.hh>
|
||||
#include <modern-qt/utility/material-icon.hh>
|
||||
#include <modern-qt/utility/theme/preset/blue-miku.hh>
|
||||
#include <modern-qt/utility/theme/theme.hh>
|
||||
#include <modern-qt/widget/cards/filled-card.hh>
|
||||
#include <modern-qt/widget/main-window.hh>
|
||||
#include <qfontdatabase.h>
|
||||
|
||||
using namespace creeper;
|
||||
|
||||
namespace lnpro = linear::pro;
|
||||
namespace mwpro = main_window::pro;
|
||||
namespace capro = card::pro;
|
||||
|
||||
auto main(int argc, char *argv[]) -> int {
|
||||
app::init {
|
||||
app::pro::Attribute {Qt::AA_EnableHighDpiScaling},
|
||||
app::pro::Attribute {Qt::AA_UseHighDpiPixmaps},
|
||||
app::pro::Complete {argc, argv},
|
||||
};
|
||||
|
||||
auto manager = ThemeManager {kBlueMikuThemePack};
|
||||
creeper::material::FontLoader::load_font();
|
||||
auto nav_component_state = NavComponentState {
|
||||
.manager = manager,
|
||||
.switch_callback = [&](int index, const auto& name) {
|
||||
|
||||
},
|
||||
.buttons_context = {
|
||||
{"0", material::icon::kHome},
|
||||
{"1", material::icon::kStar},
|
||||
{"2", material::icon::kFavorite},
|
||||
{"3", material::icon::kExtension},
|
||||
{"4", material::icon::kLogout},
|
||||
},
|
||||
};
|
||||
auto view_component_state = ViewComponentState {.manager = manager};
|
||||
auto mask_window = (MixerMask*){};
|
||||
creeper::ShowWindow<MainWindow> {
|
||||
[&](MainWindow& window) noexcept {
|
||||
|
||||
},
|
||||
mwpro::FixedSize {1080, 720},
|
||||
mwpro::Central<FilledCard> {
|
||||
capro::ThemeManager {manager},
|
||||
capro::Radius {0},
|
||||
capro::Level {CardLevel::HIGHEST},
|
||||
|
||||
capro::Layout<Row> {
|
||||
lnpro::Margin{0},
|
||||
lnpro::Spacing{0},
|
||||
lnpro::Item {
|
||||
NavComponent(nav_component_state),
|
||||
},
|
||||
lnpro::Item<Col> {
|
||||
lnpro::ContentsMargin{{15, 15, 15, 15}},
|
||||
},
|
||||
lnpro::Item<Col> {
|
||||
{255},
|
||||
lnpro::ContentsMargin { { 5, 15, 15, 15 } },
|
||||
lnpro::Item<ScrollArea> {
|
||||
scroll::pro::ThemeManager { manager },
|
||||
scroll::pro::HorizontalScrollBarPolicy {
|
||||
Qt::ScrollBarAlwaysOff,
|
||||
},
|
||||
scroll::pro::Item {
|
||||
ViewComponent(view_component_state),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
mixer::pro::SetMixerMask {mask_window},
|
||||
};
|
||||
manager.apply_theme();
|
||||
manager.append_begin_callback([=](const auto&) {
|
||||
auto const point = mask_window->mapFromGlobal(QCursor::pos());
|
||||
mask_window->initiate_animation(point);
|
||||
});
|
||||
|
||||
return app::exec();
|
||||
}
|
||||
1
modern-qt/core.hh
Normal file
1
modern-qt/core.hh
Normal file
@@ -0,0 +1 @@
|
||||
#include "core/application.hh"
|
||||
65
modern-qt/core/application.hh
Normal file
65
modern-qt/core/application.hh
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include <qapplication.h>
|
||||
#include <qcoreapplication.h>
|
||||
|
||||
namespace creeper::app::pro {
|
||||
|
||||
using Token = common::Token<QApplication>;
|
||||
|
||||
struct Complete : Token {
|
||||
|
||||
int& argument_count;
|
||||
char** argument_array;
|
||||
int application_flags;
|
||||
|
||||
explicit Complete(int& argc, char* argv[], int flags = ::QCoreApplication::ApplicationFlags)
|
||||
: argument_count { argc }
|
||||
, argument_array { argv }
|
||||
, application_flags { flags } { }
|
||||
|
||||
void apply(auto&) const noexcept {
|
||||
new ::QApplication {
|
||||
argument_count,
|
||||
argument_array,
|
||||
application_flags,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
struct Attribute : Token {
|
||||
|
||||
::Qt::ApplicationAttribute attribute;
|
||||
bool on;
|
||||
|
||||
explicit Attribute(::Qt::ApplicationAttribute attribute, bool on = true) noexcept
|
||||
: attribute { attribute }
|
||||
, on { on } { }
|
||||
|
||||
void apply(auto&) const noexcept { ::QApplication::setAttribute(attribute, on); }
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
}
|
||||
namespace creeper::app {
|
||||
|
||||
struct Application { };
|
||||
using init = Declarative<Application, pro::checker>;
|
||||
|
||||
inline auto exec() { return ::QApplication::exec(); }
|
||||
inline auto quit() { return ::QApplication::quit(); }
|
||||
|
||||
inline auto focus_widget() { return ::QApplication::focusWidget(); }
|
||||
inline auto focus_object() { return ::QApplication::focusObject(); }
|
||||
|
||||
#if QT_DEPRECATED_SINCE(6, 0)
|
||||
#define AA_EnableHighDpiScaling AA_AttributeCount
|
||||
#define AA_UseHighDpiPixmaps AA_AttributeCount
|
||||
#endif
|
||||
|
||||
}
|
||||
4
modern-qt/creeper-qt.hh
Normal file
4
modern-qt/creeper-qt.hh
Normal file
@@ -0,0 +1,4 @@
|
||||
#include "core.hh"
|
||||
#include "layout.hh"
|
||||
#include "utility.hh"
|
||||
#include "widget.hh"
|
||||
5
modern-qt/layout.hh
Normal file
5
modern-qt/layout.hh
Normal file
@@ -0,0 +1,5 @@
|
||||
#include "modern-qt/layout/group.hh"
|
||||
#include "modern-qt/layout/linear.hh"
|
||||
#include "modern-qt/layout/mutual-exclusion-group.hh"
|
||||
#include "modern-qt/layout/scroll.hh"
|
||||
#include "modern-qt/layout/stacked.hh"
|
||||
156
modern-qt/layout/flow.cc
Normal file
156
modern-qt/layout/flow.cc
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "flow.hh"
|
||||
#include <qstyle.h>
|
||||
|
||||
using namespace creeper::flow::internal;
|
||||
|
||||
struct Flow::Impl {
|
||||
QList<Item*> items;
|
||||
|
||||
int row_spacing = 10;
|
||||
int col_spacing = 10;
|
||||
|
||||
int row_limit = std::numeric_limits<int>::max();
|
||||
|
||||
Flow& self;
|
||||
|
||||
explicit Impl(Flow& self) noexcept
|
||||
: self { self } { }
|
||||
|
||||
auto calculate_spacing(QStyle::PixelMetric pm) const -> int {
|
||||
const auto parent = self.parent();
|
||||
if (!parent) {
|
||||
return -1;
|
||||
} else if (parent->isWidgetType()) {
|
||||
const auto pw = static_cast<QWidget*>(parent);
|
||||
return pw->style()->pixelMetric(pm, nullptr, pw);
|
||||
} else {
|
||||
return static_cast<QLayout*>(parent)->spacing();
|
||||
}
|
||||
}
|
||||
auto horizontal_spacing() const -> int {
|
||||
if (row_spacing >= 0) {
|
||||
return row_spacing;
|
||||
} else {
|
||||
return calculate_spacing(QStyle::PM_LayoutHorizontalSpacing);
|
||||
}
|
||||
}
|
||||
auto vertical_spacing() const -> int {
|
||||
if (col_spacing >= 0) {
|
||||
return col_spacing;
|
||||
} else {
|
||||
return calculate_spacing(QStyle::PM_LayoutVerticalSpacing);
|
||||
}
|
||||
}
|
||||
|
||||
auto update_items_geometry(Item* item, const QPoint& point) const {
|
||||
// TODO: 显然,这个接口未来可以拓展成带动画的位姿更新
|
||||
item->setGeometry({ point, item->sizeHint() });
|
||||
}
|
||||
|
||||
// Flow 理应是一个常用的布局,但 Qt 中没有对应的实现,
|
||||
// https://doc.qt.io/archives/qt-5.15/qtwidgets-layouts-flowlayout-example.html
|
||||
auto calculate_layout(const QRect& rect, bool apply) const -> int {
|
||||
|
||||
int left, top, right, bottom;
|
||||
self.getContentsMargins(&left, &top, &right, &bottom);
|
||||
|
||||
const auto effective_rect = rect.adjusted(+left, +top, -right, -bottom);
|
||||
|
||||
auto current_x = effective_rect.x();
|
||||
auto current_y = effective_rect.y();
|
||||
|
||||
auto line_height = int { 0 };
|
||||
auto line_length = int { 0 };
|
||||
for (auto item : std::as_const(items)) {
|
||||
const auto widget = item->widget();
|
||||
const auto spacing = [widget](Qt::Orientation o) {
|
||||
return widget->style()->layoutSpacing(
|
||||
QSizePolicy::PushButton, QSizePolicy::PushButton, o);
|
||||
};
|
||||
|
||||
auto space_x = row_spacing;
|
||||
if (space_x == -1) space_x = spacing(Qt::Horizontal);
|
||||
|
||||
auto space_y = col_spacing;
|
||||
if (space_y == -1) space_y = spacing(Qt::Vertical);
|
||||
|
||||
auto next_x = current_x + item->sizeHint().width() + space_x;
|
||||
|
||||
const auto area_flag = next_x - space_x > effective_rect.right();
|
||||
const auto size_flag = line_length > row_limit;
|
||||
if ((area_flag || size_flag) && line_height > 0) {
|
||||
current_x = effective_rect.x();
|
||||
current_y = current_y + line_height + space_y;
|
||||
next_x = current_x + item->sizeHint().width() + space_x;
|
||||
line_height = 0;
|
||||
line_length = 0;
|
||||
}
|
||||
|
||||
if (apply) {
|
||||
const auto point = QPoint { current_x, current_y };
|
||||
update_items_geometry(item, point);
|
||||
}
|
||||
|
||||
current_x = next_x;
|
||||
line_height = std::max(line_height, item->sizeHint().height());
|
||||
line_length = line_length + 1;
|
||||
}
|
||||
return current_y + line_height - rect.y() + bottom;
|
||||
}
|
||||
};
|
||||
|
||||
Flow::Flow()
|
||||
: pimpl { std::make_unique<Impl>(*this) } { }
|
||||
|
||||
Flow::~Flow() {
|
||||
while (auto item = Flow::takeAt(0))
|
||||
delete item;
|
||||
}
|
||||
|
||||
auto Flow::addItem(Item* item) -> void { pimpl->items.append(item); }
|
||||
|
||||
auto Flow::setGeometry(const QRect& rect) -> void {
|
||||
QLayout::setGeometry(rect);
|
||||
pimpl->calculate_layout(rect, true);
|
||||
}
|
||||
|
||||
auto Flow::takeAt(int index) -> Item* {
|
||||
auto& items = pimpl->items;
|
||||
return (index < 0 || index > items.size() - 1) ? nullptr : items.takeAt(index);
|
||||
}
|
||||
|
||||
auto Flow::expandingDirections() const -> Qt::Orientations { return {}; }
|
||||
|
||||
auto Flow::hasHeightForWidth() const -> bool { return true; }
|
||||
|
||||
auto Flow::heightForWidth(int width) const -> int {
|
||||
return pimpl->calculate_layout({ 0, 0, width, 0 }, false);
|
||||
}
|
||||
|
||||
auto Flow::itemAt(int index) const -> Item* { return pimpl->items.value(index); }
|
||||
|
||||
auto Flow::count() const -> int { return pimpl->items.size(); }
|
||||
|
||||
auto Flow::minimumSize() const -> QSize {
|
||||
auto result = QSize {};
|
||||
for (const auto item : std::as_const(pimpl->items))
|
||||
result = result.expandedTo(item->minimumSize());
|
||||
|
||||
const auto margins = contentsMargins();
|
||||
result += QSize {
|
||||
margins.left() + margins.right(),
|
||||
margins.top() + margins.bottom(),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
auto Flow::sizeHint() const -> QSize { return Flow::minimumSize(); }
|
||||
|
||||
auto Flow::set_row_spacing(int spacing) noexcept -> void { pimpl->row_spacing = spacing; }
|
||||
auto Flow::row_spacing() const noexcept -> int { return pimpl->row_spacing; }
|
||||
|
||||
auto Flow::set_col_spacing(int spacing) noexcept -> void { pimpl->col_spacing = spacing; }
|
||||
auto Flow::col_spacing() const noexcept -> int { return pimpl->col_spacing; }
|
||||
|
||||
auto Flow::set_row_limit(int limit) noexcept -> void { pimpl->row_limit = limit; }
|
||||
auto Flow::row_limit() const noexcept -> int { return pimpl->row_limit; }
|
||||
64
modern-qt/layout/flow.hh
Normal file
64
modern-qt/layout/flow.hh
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/layout.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include <qlayout.h>
|
||||
|
||||
namespace creeper::flow::internal {
|
||||
|
||||
class Flow : public QLayout {
|
||||
CREEPER_PIMPL_DEFINITION(Flow)
|
||||
|
||||
public:
|
||||
using Item = QLayoutItem;
|
||||
|
||||
auto addItem(Item*) -> void override;
|
||||
auto takeAt(int) -> Item* override;
|
||||
auto setGeometry(const QRect&) -> void override;
|
||||
|
||||
auto expandingDirections() const -> Qt::Orientations override;
|
||||
auto hasHeightForWidth() const -> bool override;
|
||||
auto heightForWidth(int) const -> int override;
|
||||
|
||||
auto itemAt(int) const -> Item* override;
|
||||
auto count() const -> int override;
|
||||
auto minimumSize() const -> QSize override;
|
||||
auto sizeHint() const -> QSize override;
|
||||
|
||||
public:
|
||||
auto set_row_spacing(int) noexcept -> void;
|
||||
auto row_spacing() const noexcept -> int;
|
||||
|
||||
auto set_col_spacing(int) noexcept -> void;
|
||||
auto col_spacing() const noexcept -> int;
|
||||
|
||||
auto set_row_limit(int) noexcept -> void;
|
||||
auto row_limit() const noexcept -> int;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::flow::pro {
|
||||
|
||||
using Token = common::Token<internal::Flow>;
|
||||
|
||||
using RowSpacing = SetterProp<Token, int, [](auto& self, int v) { self.set_row_spacing(v); }>;
|
||||
|
||||
using ColSpacing = SetterProp<Token, int, [](auto& self, int v) { self.set_col_spacing(v); }>;
|
||||
|
||||
using RowLimit = SetterProp<Token, int, [](auto& self, int v) { self.set_row_limit(v); }>;
|
||||
|
||||
using MainAxisSpacing = RowSpacing;
|
||||
using CrossAxisSpacing = ColSpacing;
|
||||
using MaxItemsInEachRow = RowLimit;
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace layout::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using Flow = Declarative<flow::internal::Flow, CheckerOr<flow::pro::checker, layout::pro::checker>>;
|
||||
|
||||
}
|
||||
0
modern-qt/layout/form.hh
Normal file
0
modern-qt/layout/form.hh
Normal file
83
modern-qt/layout/grid.hh
Normal file
83
modern-qt/layout/grid.hh
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
|
||||
#include <qgridlayout.h>
|
||||
|
||||
namespace creeper {
|
||||
namespace grid::internal {
|
||||
class Grid : public QGridLayout { };
|
||||
}
|
||||
namespace grid::pro {
|
||||
using Token = common::Token<QGridLayout>;
|
||||
|
||||
struct RowSpacing : Token { };
|
||||
|
||||
struct ColSpacing : Token { };
|
||||
|
||||
template <class T>
|
||||
requires std::is_convertible_v<T*, QWidget*> || std::is_convertible_v<T*, QLayout*>
|
||||
struct Item : Token {
|
||||
using Align = Qt::Alignment;
|
||||
|
||||
struct LayoutMethod {
|
||||
int row = 0, row_span = 0;
|
||||
int col = 0, col_span = 0;
|
||||
Align align;
|
||||
|
||||
explicit LayoutMethod(int row, int col, Align align = {})
|
||||
: row { row }
|
||||
, col { col }
|
||||
, align { align } { }
|
||||
|
||||
explicit LayoutMethod(int row, int row_span, int col, int col_span, Align align = {})
|
||||
: row { row }
|
||||
, col { col }
|
||||
, row_span { row_span }
|
||||
, col_span { col_span }
|
||||
, align { align } { }
|
||||
|
||||
} method;
|
||||
|
||||
T* item_pointer = nullptr;
|
||||
|
||||
explicit Item(const LayoutMethod& method, auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: item_pointer { new T { std::forward<decltype(args)>(args)... } }
|
||||
, method(method) { }
|
||||
|
||||
explicit Item(const LayoutMethod& method, T* pointer) noexcept
|
||||
: item_pointer { pointer }
|
||||
, method { method } { }
|
||||
|
||||
void apply(QGridLayout& layout) const {
|
||||
if (method.col_span == 0) {
|
||||
if constexpr (std::is_convertible_v<T*, QWidget*>)
|
||||
layout.addWidget(item_pointer, method.row, method.col, method.align);
|
||||
if constexpr (std::is_convertible_v<T*, QLayout*>)
|
||||
layout.addLayout(item_pointer, method.row, method.col, method.align);
|
||||
} else {
|
||||
if constexpr (std::is_convertible_v<T*, QWidget*>)
|
||||
layout.addWidget(item_pointer, method.row, method.row_span, method.col,
|
||||
method.col_span, method.align);
|
||||
if constexpr (std::is_convertible_v<T*, QLayout*>)
|
||||
layout.addLayout(item_pointer, method.row, method.row_span, method.col,
|
||||
method.col_span, method.align);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Items : Token {
|
||||
explicit Items() { }
|
||||
void apply(QGridLayout& self) const { }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
|
||||
}
|
||||
using Grid = Declarative<grid::internal::Grid, grid::pro::checker>;
|
||||
}
|
||||
124
modern-qt/layout/group.hh
Normal file
124
modern-qt/layout/group.hh
Normal file
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/trait/widget.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
|
||||
namespace creeper::group::internal {
|
||||
|
||||
template <typename F, typename T>
|
||||
concept foreach_invoke_item_trait = requires {
|
||||
{ std::invoke(std::declval<F>(), std::declval<T const&>()) } -> widget_pointer_trait;
|
||||
};
|
||||
|
||||
template <typename F, typename T>
|
||||
concept foreach_apply_item_trait = requires {
|
||||
{ std::apply(std::declval<F>(), std::declval<T const&>()) } -> widget_pointer_trait;
|
||||
};
|
||||
|
||||
template <typename F, typename T>
|
||||
concept foreach_item_trait = foreach_invoke_item_trait<F, T> || foreach_apply_item_trait<F, T>;
|
||||
|
||||
template <typename R, typename F>
|
||||
concept foreach_invoke_ranges_trait = foreach_item_trait<F, std::ranges::range_value_t<R>>;
|
||||
|
||||
template <layout_trait T, widget_trait W>
|
||||
struct Group : public T {
|
||||
using T::T;
|
||||
std::vector<W*> widgets;
|
||||
|
||||
template <std::ranges::range R, typename F>
|
||||
requires foreach_invoke_ranges_trait<R, F>
|
||||
constexpr auto compose(const R& ranges, F&& f, Qt::Alignment a = {}) noexcept -> void {
|
||||
for (const auto& item : ranges) {
|
||||
using ItemT = decltype(item);
|
||||
|
||||
auto widget_pointer = (W*) {};
|
||||
|
||||
if constexpr (foreach_invoke_item_trait<F, ItemT>)
|
||||
widget_pointer = std::invoke(f, item);
|
||||
|
||||
else if constexpr (foreach_apply_item_trait<F, ItemT>)
|
||||
widget_pointer = std::apply(f, item);
|
||||
|
||||
if (widget_pointer != nullptr) {
|
||||
T::addWidget(widget_pointer, 0, a);
|
||||
widgets.push_back(widget_pointer);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto foreach_(this auto&& self, auto&& f) noexcept
|
||||
requires std::invocable<decltype(f), W&>
|
||||
{
|
||||
for (auto widget : self.widgets)
|
||||
std::invoke(f, *widget);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
namespace creeper::group::pro {
|
||||
|
||||
using Token = common::Token<internal::Group<QLayout, QWidget>>;
|
||||
|
||||
/// @note
|
||||
/// 一种典型的用法,委托构造时,所传函数只能接受常量引用,
|
||||
/// 放心使用 auto,类型是可以被推导出来的
|
||||
///
|
||||
/// group::pro::Compose {
|
||||
/// std::array {
|
||||
/// std::tuple(1, "xxxxxx"),
|
||||
/// ......
|
||||
/// },
|
||||
/// [](auto index, auto text) {
|
||||
/// return new TextButton { ... };
|
||||
/// },
|
||||
/// }
|
||||
///
|
||||
template <typename R, typename F>
|
||||
requires internal::foreach_invoke_ranges_trait<R, F>
|
||||
struct Compose : Token {
|
||||
const R& ranges;
|
||||
F method;
|
||||
Qt::Alignment alignment;
|
||||
|
||||
explicit Compose(const R& r, F f, Qt::Alignment a = {}) noexcept
|
||||
: ranges { r }
|
||||
, method { std::move(f) }
|
||||
, alignment { a } { }
|
||||
|
||||
auto apply(auto& self) noexcept -> void { //
|
||||
self.compose(ranges, std::move(method), alignment);
|
||||
}
|
||||
};
|
||||
|
||||
/// @note
|
||||
/// 函数参数是组件的引用:
|
||||
///
|
||||
/// group::pro::Foreach { [](Widget& button) { ... } },
|
||||
///
|
||||
template <typename F>
|
||||
requires(!std::invocable<F>)
|
||||
struct Foreach : Token {
|
||||
F function;
|
||||
|
||||
explicit Foreach(F&& f) noexcept
|
||||
: function { std::forward<F>(f) } { }
|
||||
|
||||
auto apply(auto& self) const noexcept {
|
||||
// 很遗憾,Qt 占用了 foreach 这个单词
|
||||
self.foreach_(std::move(function));
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait)
|
||||
};
|
||||
namespace creeper {
|
||||
|
||||
template <layout_trait T, widget_trait W>
|
||||
using Group =
|
||||
Declarative<group::internal::Group<T, W>, CheckerOr<group::pro::checker, typename T::Checker>>;
|
||||
|
||||
}
|
||||
108
modern-qt/layout/linear.hh
Normal file
108
modern-qt/layout/linear.hh
Normal file
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/trait/widget.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/layout.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
|
||||
#include <qboxlayout.h>
|
||||
|
||||
namespace creeper::linear::pro {
|
||||
|
||||
using Token = common::Token<QBoxLayout>;
|
||||
|
||||
struct SpacingItem : Token {
|
||||
int size;
|
||||
explicit SpacingItem(int p) { size = p; }
|
||||
void apply(QBoxLayout& self) const { self.addSpacing(size); }
|
||||
};
|
||||
|
||||
struct Stretch : Token {
|
||||
int stretch;
|
||||
explicit Stretch(int p) { stretch = p; }
|
||||
void apply(QBoxLayout& self) const { self.addStretch(stretch); }
|
||||
};
|
||||
|
||||
struct SpacerItem : Token {
|
||||
QSpacerItem* spacer_item;
|
||||
explicit SpacerItem(QSpacerItem* p) { spacer_item = p; }
|
||||
void apply(QBoxLayout& self) const { self.addSpacerItem(spacer_item); }
|
||||
};
|
||||
|
||||
/// @brief
|
||||
/// 布局项包装器,用于声明式地将 Widget 或 Layout 添加到布局中
|
||||
///
|
||||
/// @tparam T
|
||||
/// 被包装的组件类型,需满足可转换为 QWidget* 或 QLayout*,不需
|
||||
/// 要显式指定,由构造参数推倒
|
||||
///
|
||||
/// @note
|
||||
/// Item 提供统一的接口用于在布局中插入控件或子布局,
|
||||
/// 支持多种构造方式,包括直接传入指针或通过参数构造新对象。
|
||||
/// 通过 LayoutMethod 可指定拉伸因子和对齐方式,
|
||||
/// 在布局应用时自动选择 addWidget 或 addLayout,
|
||||
/// 实现非侵入式的布局声明式封装。
|
||||
///
|
||||
/// 示例用途:
|
||||
/// linear::pro::Item<Widget> {
|
||||
/// { 0, Qt::AlignHCenter } // stretch, and alignment, optional
|
||||
/// ...
|
||||
/// };
|
||||
///
|
||||
template <item_trait T>
|
||||
struct Item : Token {
|
||||
struct LayoutMethod {
|
||||
int stretch = 0;
|
||||
Qt::Alignment align = {};
|
||||
} method;
|
||||
|
||||
T* item_pointer = nullptr;
|
||||
|
||||
explicit Item(const LayoutMethod& method, T* pointer) noexcept
|
||||
: item_pointer { pointer }
|
||||
, method { method } { }
|
||||
|
||||
explicit Item(T* pointer) noexcept
|
||||
: item_pointer { pointer } { }
|
||||
|
||||
explicit Item(const LayoutMethod& method, auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: item_pointer { new T { std::forward<decltype(args)>(args)... } }
|
||||
, method(method) { }
|
||||
|
||||
explicit Item(auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: item_pointer { new T { std::forward<decltype(args)>(args)... } } { }
|
||||
|
||||
void apply(linear_trait auto& layout) const {
|
||||
if constexpr (widget_trait<T>) layout.addWidget(item_pointer, method.stretch, method.align);
|
||||
if constexpr (layout_trait<T>) layout.addLayout(item_pointer, method.stretch);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace layout::pro;
|
||||
}
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <class T>
|
||||
using BoxLayout = Declarative<T, CheckerOr<linear::pro::checker, layout::pro::checker>>;
|
||||
|
||||
using Row = BoxLayout<QHBoxLayout>;
|
||||
using Col = BoxLayout<QVBoxLayout>;
|
||||
|
||||
namespace row = linear;
|
||||
namespace col = linear;
|
||||
|
||||
namespace internal {
|
||||
inline auto use_the_namespace_alias_to_eliminate_warnings() {
|
||||
std::ignore = row::pro::Token {};
|
||||
std::ignore = col::pro::Token {};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
3
modern-qt/layout/mixer.cc
Normal file
3
modern-qt/layout/mixer.cc
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "mixer.hh"
|
||||
|
||||
using namespace creeper::mixer::internal;
|
||||
112
modern-qt/layout/mixer.hh
Normal file
112
modern-qt/layout/mixer.hh
Normal file
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include <qpainter.h>
|
||||
#include <qpainterpath.h>
|
||||
|
||||
#include "modern-qt/utility/animation/animatable.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
|
||||
namespace creeper::mixer::internal {
|
||||
|
||||
class MixerMask : public QWidget {
|
||||
public:
|
||||
explicit MixerMask(auto* widget) noexcept
|
||||
: QWidget { widget }
|
||||
, animatable { *this } {
|
||||
|
||||
QWidget::setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
mask_frame.fill(Qt::transparent);
|
||||
{
|
||||
auto state = std::make_shared<PidState<double>>();
|
||||
|
||||
state->config.kp = 05.0;
|
||||
state->config.ki = 00.0;
|
||||
state->config.kd = 00.0;
|
||||
state->config.epsilon = 1e-3;
|
||||
|
||||
mask_radius = make_transition(animatable, std::move(state));
|
||||
}
|
||||
}
|
||||
|
||||
auto initiate_animation(QPoint const& point) noexcept {
|
||||
mask_frame.fill(Qt::transparent);
|
||||
|
||||
auto* widget = parentWidget();
|
||||
if (widget == nullptr) return;
|
||||
|
||||
mask_radius->snap_to(0.);
|
||||
mask_radius->transition_to(1.);
|
||||
|
||||
mask_point = point;
|
||||
mask_frame = widget->grab();
|
||||
|
||||
update_animation = true;
|
||||
QWidget::setFixedSize(widget->size());
|
||||
}
|
||||
auto initiate_animation(int x, int y) noexcept {
|
||||
// Forward Point
|
||||
initiate_animation(QPoint { x, y });
|
||||
}
|
||||
|
||||
protected:
|
||||
auto paintEvent(QPaintEvent* e) -> void override {
|
||||
if (!update_animation) return;
|
||||
|
||||
auto const w = QWidget::width();
|
||||
auto const h = QWidget::height();
|
||||
auto const x = std::sqrt(w * w + h * h);
|
||||
|
||||
auto painter = QPainter { this };
|
||||
|
||||
auto const radius = double { *mask_radius * x };
|
||||
auto const round = [&] {
|
||||
auto path = QPainterPath {};
|
||||
path.addRect(QWidget::rect());
|
||||
|
||||
auto inner = QPainterPath();
|
||||
inner.addEllipse(mask_point, radius, radius);
|
||||
|
||||
return path.subtracted(inner);
|
||||
}();
|
||||
painter.setClipPath(round);
|
||||
painter.setClipping(true);
|
||||
|
||||
painter.drawPixmap(QWidget::rect(), mask_frame);
|
||||
|
||||
if (std::abs(*mask_radius - 1.) < 1e-2) {
|
||||
update_animation = false;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QPixmap mask_frame;
|
||||
QPointF mask_point;
|
||||
|
||||
bool update_animation = false;
|
||||
|
||||
Animatable animatable;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> mask_radius;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::mixer::pro {
|
||||
|
||||
struct SetMixerMask : widget::pro::Token {
|
||||
internal::MixerMask*& mask;
|
||||
explicit SetMixerMask(auto*& mask)
|
||||
: mask { mask } { }
|
||||
auto apply(auto& self) noexcept {
|
||||
//
|
||||
mask = new internal::MixerMask { &self };
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using MixerMask = mixer::internal::MixerMask;
|
||||
|
||||
}
|
||||
98
modern-qt/layout/mutual-exclusion-group.hh
Normal file
98
modern-qt/layout/mutual-exclusion-group.hh
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
#include <modern-qt/layout/group.hh>
|
||||
#include <modern-qt/utility/wrapper/layout.hh>
|
||||
#include <ranges>
|
||||
|
||||
namespace creeper::mutual_exclusion_group::internal {
|
||||
|
||||
template <auto f, typename W>
|
||||
concept switch_function_trait = std::invocable<decltype(f), W&, bool>;
|
||||
|
||||
template <layout_trait T, widget_trait W, auto switch_function>
|
||||
requires switch_function_trait<switch_function, W>
|
||||
struct MutualExclusionGroup : public Group<T, W> {
|
||||
using Group<T, W>::Group;
|
||||
|
||||
public:
|
||||
auto switch_widgets(std::size_t index) const noexcept {
|
||||
for (auto [index_, w] : std::views::enumerate(this->widgets)) {
|
||||
switch_function(*w, index_ == index);
|
||||
}
|
||||
}
|
||||
auto switch_widgets(W* widget) const noexcept {
|
||||
for (auto w_ : this->widgets) {
|
||||
switch_function(*w_, w_ == widget);
|
||||
}
|
||||
}
|
||||
|
||||
auto make_signal_injection(auto signal) const noexcept -> void {
|
||||
for (auto widget : this->widgets) {
|
||||
QObject::connect(widget, signal, [this, widget] { switch_widgets(widget); });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
constexpr inline auto checked_switch_function = [](auto& w, bool on) { w.set_checked(on); };
|
||||
constexpr inline auto opened_switch_function = [](auto& w, bool on) { w.set_opened(on); };
|
||||
constexpr inline auto selected_switch_function = [](auto& w, bool on) { w.set_selected(on); };
|
||||
|
||||
constexpr inline auto no_action_as_token = [](auto& w, bool on) { };
|
||||
|
||||
}
|
||||
namespace creeper::mutual_exclusion_group::pro {
|
||||
|
||||
struct TokenContext { };
|
||||
using Token = common::Token<TokenContext>;
|
||||
|
||||
template <typename Signal>
|
||||
struct SignalInjection : Token {
|
||||
Signal signal;
|
||||
|
||||
explicit SignalInjection(Signal signal) noexcept
|
||||
: signal { signal } { }
|
||||
|
||||
auto apply(auto& self) const noexcept -> void {
|
||||
self.make_signal_injection(signal); //
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token> || group::pro::trait<T>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace group::pro;
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
template <layout_trait T, widget_trait W, auto switch_function>
|
||||
requires mutual_exclusion_group::internal::switch_function_trait<switch_function, W>
|
||||
using MutualExclusionGroup =
|
||||
mutual_exclusion_group::internal::MutualExclusionGroup<T, W, switch_function>;
|
||||
|
||||
template <layout_trait T, widget_trait W>
|
||||
using CheckGroup = Declarative<
|
||||
MutualExclusionGroup<T, W, mutual_exclusion_group::internal::checked_switch_function>,
|
||||
CheckerOr<mutual_exclusion_group::pro::checker, typename T::Checker>>;
|
||||
namespace check_group = mutual_exclusion_group;
|
||||
|
||||
template <layout_trait T, widget_trait W>
|
||||
using OpenGroup = Declarative<
|
||||
MutualExclusionGroup<T, W, mutual_exclusion_group::internal::opened_switch_function>,
|
||||
CheckerOr<mutual_exclusion_group::pro::checker, typename T::Checker>>;
|
||||
namespace open_group = mutual_exclusion_group;
|
||||
|
||||
template <layout_trait T, widget_trait W>
|
||||
using SelectGroup = Declarative<
|
||||
MutualExclusionGroup<T, W, mutual_exclusion_group::internal::selected_switch_function>,
|
||||
CheckerOr<mutual_exclusion_group::pro::checker, typename T::Checker>>;
|
||||
namespace select_group = mutual_exclusion_group;
|
||||
|
||||
namespace internal {
|
||||
inline auto use_mutual_exclusion_group_namespace() {
|
||||
std::ignore = check_group::pro::Token {};
|
||||
std::ignore = open_group::pro::Token {};
|
||||
std::ignore = select_group::pro::Token {};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
135
modern-qt/layout/scroll.hh
Normal file
135
modern-qt/layout/scroll.hh
Normal file
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/trait/widget.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-qt/widget/widget.hh"
|
||||
#include <qscrollarea.h>
|
||||
#include <qscrollbar.h>
|
||||
|
||||
namespace creeper::scroll::internal {
|
||||
|
||||
/// NOTE: 先拿 qss 勉强用着吧,找时间完全重构
|
||||
class ScrollArea : public QScrollArea {
|
||||
public:
|
||||
explicit ScrollArea() noexcept {
|
||||
viewport()->setStyleSheet("background:transparent;");
|
||||
setStyleSheet("QScrollArea{background:transparent;}");
|
||||
|
||||
setWidgetResizable(true);
|
||||
}
|
||||
|
||||
void set_color_scheme(const ColorScheme& scheme) {
|
||||
constexpr auto q = [](const QColor& c, int a = 255) {
|
||||
return QString("rgba(%1,%2,%3,%4)").arg(c.red()).arg(c.green()).arg(c.blue()).arg(a);
|
||||
};
|
||||
|
||||
verticalScrollBar()->setStyleSheet(QString {
|
||||
"QScrollBar:vertical{background:transparent;width:8px;border-radius:4px;}"
|
||||
"QScrollBar::handle:vertical{background:%1;min-height:20px;border-radius:4px;}"
|
||||
"QScrollBar::handle:vertical:hover{background:%2;}"
|
||||
"QScrollBar::handle:vertical:pressed{background:%3;}"
|
||||
"QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical,"
|
||||
"QScrollBar::add-page:vertical,QScrollBar::sub-page:vertical{height:0px;}",
|
||||
}
|
||||
.arg(q(scheme.primary, 235))
|
||||
.arg(q(scheme.primary))
|
||||
.arg(q(scheme.primary.darker(110))));
|
||||
|
||||
horizontalScrollBar()->setStyleSheet(QString {
|
||||
"QScrollBar:horizontal{background:transparent;height:8px;border-radius:4px;}"
|
||||
"QScrollBar::handle:horizontal{background:%1;min-width:20px;border-radius:4px;}"
|
||||
"QScrollBar::handle:horizontal:hover{background:%2;}"
|
||||
"QScrollBar::handle:horizontal:pressed{background:%3;}"
|
||||
"QScrollBar::add-line:horizontal,QScrollBar::sub-line:horizontal,"
|
||||
"QScrollBar::add-page:horizontal,QScrollBar::sub-page:horizontal{width:0px;}",
|
||||
}
|
||||
.arg(q(scheme.primary, 235))
|
||||
.arg(q(scheme.primary))
|
||||
.arg(q(scheme.primary.darker(110))));
|
||||
}
|
||||
|
||||
void load_theme_manager(ThemeManager& manager) {
|
||||
manager.append_handler(this,
|
||||
[this](const ThemeManager& manager) { set_color_scheme(manager.color_scheme()); });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::scroll::pro {
|
||||
|
||||
using Token = common::Token<internal::ScrollArea>;
|
||||
|
||||
struct VerticalScrollBarPolicy : Token {
|
||||
Qt::ScrollBarPolicy policy;
|
||||
|
||||
explicit VerticalScrollBarPolicy(Qt::ScrollBarPolicy policy) noexcept
|
||||
: policy { policy } { }
|
||||
|
||||
auto apply(auto& self) const noexcept -> void { //
|
||||
self.setVerticalScrollBarPolicy(policy);
|
||||
}
|
||||
};
|
||||
struct HorizontalScrollBarPolicy : Token {
|
||||
Qt::ScrollBarPolicy policy;
|
||||
|
||||
explicit HorizontalScrollBarPolicy(Qt::ScrollBarPolicy policy) noexcept
|
||||
: policy { policy } { }
|
||||
|
||||
auto apply(auto& self) const noexcept -> void { //
|
||||
self.setHorizontalScrollBarPolicy(policy);
|
||||
}
|
||||
};
|
||||
struct ScrollBarPolicy : Token {
|
||||
Qt::ScrollBarPolicy v;
|
||||
Qt::ScrollBarPolicy h;
|
||||
|
||||
explicit ScrollBarPolicy(Qt::ScrollBarPolicy v, Qt::ScrollBarPolicy h) noexcept
|
||||
: v { v }
|
||||
, h { h } { }
|
||||
|
||||
auto apply(auto& self) const noexcept -> void {
|
||||
self.setVerticalScrollBarPolicy(v);
|
||||
self.setHorizontalScrollBarPolicy(h);
|
||||
}
|
||||
};
|
||||
|
||||
template <item_trait T>
|
||||
struct Item : Token {
|
||||
T* item_pointer = nullptr;
|
||||
|
||||
explicit Item(auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: item_pointer { new T { std::forward<decltype(args)>(args)... } } { }
|
||||
|
||||
explicit Item(T* pointer) noexcept
|
||||
: item_pointer { pointer } { }
|
||||
|
||||
auto apply(area_trait auto& layout) const noexcept -> void {
|
||||
if constexpr (widget_trait<T>) {
|
||||
layout.setWidget(item_pointer);
|
||||
}
|
||||
// NOTE: 这里可能有调整的空间,直接设置 Layout,
|
||||
// 布局 Size 行为是不正确的
|
||||
else if constexpr (layout_trait<T>) {
|
||||
const auto content = new Widget {
|
||||
widget::pro::Layout { item_pointer },
|
||||
};
|
||||
layout.setWidget(content);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 ScrollArea = Declarative<scroll::internal::ScrollArea,
|
||||
CheckerOr<scroll::pro::checker, widget::pro::checker, theme::pro::checker>>;
|
||||
|
||||
}
|
||||
26
modern-qt/layout/stacked.hh
Normal file
26
modern-qt/layout/stacked.hh
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/layout.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
|
||||
#include <qstackedlayout.h>
|
||||
|
||||
namespace creeper::stacked::internal {
|
||||
class Stacked : public QStackedLayout { };
|
||||
}
|
||||
|
||||
namespace creeper::stacked::pro {
|
||||
|
||||
using Token = common::Token<internal::Stacked>;
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token> || layout::pro::trait<T>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace layout::pro;
|
||||
}
|
||||
|
||||
namespace creeper {
|
||||
using Stacked = Declarative<stacked::internal::Stacked, stacked::pro::checker>;
|
||||
using NavHost = Stacked;
|
||||
}
|
||||
16
modern-qt/utility.hh
Normal file
16
modern-qt/utility.hh
Normal file
@@ -0,0 +1,16 @@
|
||||
#include "utility/animation/animatable.hh"
|
||||
#include "utility/animation/core.hh"
|
||||
#include "utility/animation/transition.hh"
|
||||
#include "utility/animation/water-ripple.hh"
|
||||
|
||||
#include "utility/painter/helper.hh"
|
||||
|
||||
#include "utility/theme/preset/blue-miku.hh"
|
||||
#include "utility/theme/theme.hh"
|
||||
|
||||
#include "utility/wrapper/mutable.hh"
|
||||
|
||||
#include "utility/content-scale.hh"
|
||||
#include "utility/material-icon.hh"
|
||||
#include "utility/painter-resource.hh"
|
||||
#include "utility/trait/widget.hh"
|
||||
54
modern-qt/utility/animation/animatable.cc
Normal file
54
modern-qt/utility/animation/animatable.cc
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "animatable.hh"
|
||||
#include <qdebug.h>
|
||||
using namespace creeper;
|
||||
|
||||
#include <qtimer.h>
|
||||
using qwidget = QWidget;
|
||||
using qtimer = QTimer;
|
||||
|
||||
struct Animatable::Impl {
|
||||
|
||||
qwidget& component;
|
||||
qtimer scheduler;
|
||||
|
||||
std::vector<std::unique_ptr<ITransitionTask>> transition_tasks;
|
||||
|
||||
explicit Impl(auto& component, int hz = 90) noexcept
|
||||
: component { component } {
|
||||
scheduler.connect(&scheduler, &qtimer::timeout, [this] { update(); });
|
||||
scheduler.setInterval(1'000 / hz);
|
||||
}
|
||||
|
||||
auto set_frame_rate(int hz) noexcept -> void { scheduler.setInterval(1'000 / hz); }
|
||||
|
||||
auto push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void {
|
||||
transition_tasks.push_back(std::move(task));
|
||||
if (!scheduler.isActive()) scheduler.start();
|
||||
}
|
||||
|
||||
auto update() noexcept -> void {
|
||||
|
||||
const auto [first, last] = std::ranges::remove_if(transition_tasks,
|
||||
[](const std::unique_ptr<ITransitionTask>& task) { return !task->update(); });
|
||||
|
||||
component.update();
|
||||
|
||||
transition_tasks.erase(first, last);
|
||||
|
||||
if (transition_tasks.empty()) {
|
||||
scheduler.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Animatable::Animatable(QWidget& component) noexcept
|
||||
: pimpl { std::make_unique<Impl>(component) } { }
|
||||
|
||||
Animatable::~Animatable() = default;
|
||||
|
||||
auto Animatable::set_frame_rate(int hz) noexcept -> void {
|
||||
pimpl->set_frame_rate(hz); //
|
||||
}
|
||||
auto Animatable::push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void {
|
||||
pimpl->push_transition_task(std::move(task));
|
||||
}
|
||||
27
modern-qt/utility/animation/animatable.hh
Normal file
27
modern-qt/utility/animation/animatable.hh
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include <qwidget.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
/// @note
|
||||
/// Ends after the calculation is completed or the controller call ends
|
||||
struct ITransitionTask {
|
||||
virtual ~ITransitionTask() noexcept = default;
|
||||
virtual auto update() noexcept -> bool = 0;
|
||||
};
|
||||
|
||||
class Animatable {
|
||||
CREEPER_PIMPL_DEFINITION(Animatable)
|
||||
|
||||
public:
|
||||
explicit Animatable(QWidget& widget) noexcept;
|
||||
|
||||
auto set_frame_rate(int hz) noexcept -> void;
|
||||
auto get_frame_rate() const noexcept -> int;
|
||||
|
||||
auto push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void;
|
||||
};
|
||||
|
||||
}
|
||||
95
modern-qt/utility/animation/math.hh
Normal file
95
modern-qt/utility/animation/math.hh
Normal file
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include <eigen3/Eigen/Dense>
|
||||
#include <qcolor.h>
|
||||
#include <qrect.h>
|
||||
|
||||
namespace creeper::animate {
|
||||
|
||||
template <typename T>
|
||||
constexpr auto zero() noexcept {
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return T { 0 };
|
||||
} else if constexpr (requires { T::Zero(); }) {
|
||||
return T::Zero();
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0, "zero() not implemented for this type");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr auto magnitude(const T& error) noexcept {
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return std::abs(error);
|
||||
} else if constexpr (requires { error.norm(); }) {
|
||||
return std::abs(error.norm());
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0, "magnitude() not implemented for this type");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr auto normalize(const T& error) noexcept {
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return error;
|
||||
} else if constexpr (requires { error.norm(); }) {
|
||||
return error.norm();
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0, "magnitude() not implemented for this type");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
constexpr auto interpolate(const T& start, const T& end, double t) noexcept -> T {
|
||||
|
||||
const auto clamped_t = std::clamp(t, 0., 1.);
|
||||
|
||||
if constexpr (std::is_arithmetic_v<T>) {
|
||||
return static_cast<T>(start + (end - start) * clamped_t);
|
||||
} else if constexpr ( //
|
||||
requires(const T& a, const T& b, const double f) {
|
||||
{ a - b } -> std::convertible_to<T>;
|
||||
{ a * f } -> std::convertible_to<T>;
|
||||
{ a + b } -> std::convertible_to<T>;
|
||||
}) {
|
||||
return start + (end - start) * clamped_t;
|
||||
} else {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"interpolate() requires T to be an arithmetic type or define +, -, and scalar "
|
||||
"multiplication.");
|
||||
}
|
||||
}
|
||||
|
||||
constexpr auto interpolate(const QRectF& start, const QRectF& end, double position) -> QRectF {
|
||||
position = qBound(0.0, position, 1.0);
|
||||
auto _1 = start.left() + (end.left() - start.left()) * position;
|
||||
auto _2 = start.top() + (end.top() - start.top()) * position;
|
||||
auto _3 = start.width() + (end.width() - start.width()) * position;
|
||||
auto _4 = start.height() + (end.height() - start.height()) * position;
|
||||
return { _1, _2, _3, _4 };
|
||||
}
|
||||
}
|
||||
|
||||
namespace creeper {
|
||||
|
||||
constexpr auto from_color(const QColor& color) -> Eigen::Vector4d {
|
||||
return Eigen::Vector4d(color.red(), color.green(), color.blue(), color.alpha());
|
||||
}
|
||||
constexpr auto from_vector4(const Eigen::Vector4d& vector) -> QColor {
|
||||
return QColor(vector[0], vector[1], vector[2], vector[3]);
|
||||
}
|
||||
|
||||
constexpr auto extract_rect(const QRectF& rect, double w_weight, double h_weight) -> QRectF {
|
||||
double rw, rh;
|
||||
if (rect.width() * h_weight > rect.height() * w_weight) {
|
||||
rh = rect.height();
|
||||
rw = rh * w_weight / h_weight;
|
||||
} else {
|
||||
rw = rect.width();
|
||||
rh = rw * h_weight / w_weight;
|
||||
}
|
||||
const auto center = rect.center();
|
||||
return QRectF(center.x() - rw / 2, center.y() - rh / 2, rw, rh);
|
||||
}
|
||||
|
||||
}
|
||||
12
modern-qt/utility/animation/state/accessor.hh
Normal file
12
modern-qt/utility/animation/state/accessor.hh
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
namespace creeper {
|
||||
|
||||
struct NormalAccessor {
|
||||
auto get_value(this auto const& self) { return self.value; }
|
||||
auto set_value(this auto& self, auto const& t) { self.value = t; }
|
||||
auto get_target(this auto const& self) { return self.target; }
|
||||
auto set_target(this auto& self, auto const& t) { self.target = t; }
|
||||
};
|
||||
|
||||
}
|
||||
75
modern-qt/utility/animation/state/linear.hh
Normal file
75
modern-qt/utility/animation/state/linear.hh
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/animation/math.hh"
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <typename T>
|
||||
struct LinearState : public NormalAccessor {
|
||||
using ValueT = T;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
T value = animate::zero<T>();
|
||||
T target = animate::zero<T>();
|
||||
|
||||
struct {
|
||||
double speed = 1.0;
|
||||
double epsilon = 1e-2;
|
||||
} config;
|
||||
|
||||
struct {
|
||||
TimePoint last_timestamp;
|
||||
} details;
|
||||
|
||||
auto set_target(T new_target) noexcept -> void {
|
||||
target = new_target;
|
||||
|
||||
const auto current_time = Clock::now();
|
||||
using namespace std::chrono_literals;
|
||||
const auto threshold = 16ms;
|
||||
|
||||
const auto elapsed_time = current_time - details.last_timestamp;
|
||||
|
||||
if (elapsed_time > threshold) {
|
||||
details.last_timestamp = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
const auto now = Clock::now();
|
||||
const auto duration = now - details.last_timestamp;
|
||||
const auto dt = std::chrono::duration<double>(duration).count();
|
||||
|
||||
if (dt <= 0.0) {
|
||||
details.last_timestamp = now;
|
||||
return animate::magnitude(target - value) > config.epsilon;
|
||||
}
|
||||
|
||||
const auto delta = target - value;
|
||||
const auto dist = animate::magnitude(delta);
|
||||
|
||||
if (dist <= config.epsilon) {
|
||||
value = target;
|
||||
details.last_timestamp = now;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto direction = animate::normalize(delta);
|
||||
const auto step = config.speed * dt;
|
||||
|
||||
if (step >= dist) {
|
||||
value = target;
|
||||
} else {
|
||||
value += direction * step;
|
||||
}
|
||||
|
||||
details.last_timestamp = now;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
94
modern-qt/utility/animation/state/pid.hh
Normal file
94
modern-qt/utility/animation/state/pid.hh
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/animation/math.hh"
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <typename T>
|
||||
struct PidState : public NormalAccessor {
|
||||
using ValueT = T;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
T value = animate::zero<T>();
|
||||
T target = animate::zero<T>();
|
||||
|
||||
struct {
|
||||
double kp = 1.0;
|
||||
double ki = 0.0;
|
||||
double kd = 0.1;
|
||||
double epsilon = 1e-3;
|
||||
} config;
|
||||
|
||||
struct {
|
||||
T integral_error = animate::zero<T>();
|
||||
T last_error = animate::zero<T>();
|
||||
TimePoint last_timestamp;
|
||||
} details;
|
||||
|
||||
auto set_target(T new_target) noexcept -> void {
|
||||
target = new_target;
|
||||
|
||||
const auto current_time = Clock::now();
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
const auto threshold = 16ms;
|
||||
|
||||
const auto elapsed_time = current_time - details.last_timestamp;
|
||||
|
||||
if (elapsed_time > threshold) {
|
||||
details.last_error = target - value;
|
||||
details.last_timestamp = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
|
||||
const auto kp = config.kp;
|
||||
const auto ki = config.ki;
|
||||
const auto kd = config.kd;
|
||||
|
||||
const auto now = Clock::now();
|
||||
const auto duration = now - details.last_timestamp;
|
||||
|
||||
const auto dt = std::chrono::duration<double>(duration).count();
|
||||
|
||||
if (dt <= 0.0) {
|
||||
details.last_timestamp = now;
|
||||
return animate::magnitude(target - value) > config.epsilon;
|
||||
}
|
||||
|
||||
const auto current_error = target - value;
|
||||
|
||||
if (animate::magnitude(current_error) <= config.epsilon
|
||||
&& animate::magnitude(details.last_error) <= config.epsilon) {
|
||||
value = target;
|
||||
details.integral_error = animate::zero<T>();
|
||||
details.last_error = animate::zero<T>();
|
||||
details.last_timestamp = now;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto proportional_term = kp * current_error;
|
||||
|
||||
details.integral_error += current_error * dt;
|
||||
const auto integral_term = ki * details.integral_error;
|
||||
|
||||
const auto derivative_error = (current_error - details.last_error) / dt;
|
||||
const auto derivative_term = kd * derivative_error;
|
||||
|
||||
const auto output = proportional_term + integral_term + derivative_term;
|
||||
|
||||
value += output * dt;
|
||||
|
||||
details.last_error = current_error;
|
||||
details.last_timestamp = now;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
72
modern-qt/utility/animation/state/spring.hh
Normal file
72
modern-qt/utility/animation/state/spring.hh
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/animation/math.hh"
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <typename T>
|
||||
struct SpringState : public NormalAccessor {
|
||||
using ValueT = T;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
T value;
|
||||
T target;
|
||||
|
||||
T velocity = animate::zero<T>();
|
||||
|
||||
TimePoint last_timestamp = Clock::now();
|
||||
|
||||
struct {
|
||||
double k = 1.0;
|
||||
double d = 0.1;
|
||||
double epsilon = 1e-1;
|
||||
} config;
|
||||
|
||||
auto set_target(T new_target) noexcept -> void {
|
||||
target = new_target;
|
||||
|
||||
const auto current_time = Clock::now();
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
const auto threshold = 16ms;
|
||||
|
||||
const auto elapsed_time = current_time - last_timestamp;
|
||||
|
||||
if (elapsed_time > threshold) {
|
||||
const auto error = target - value;
|
||||
velocity = animate::zero<T>();
|
||||
last_timestamp = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
const auto now = Clock::now();
|
||||
const auto duration = now - last_timestamp;
|
||||
const double dt = std::chrono::duration<double>(duration).count();
|
||||
|
||||
if (dt <= 0.0) {
|
||||
last_timestamp = now;
|
||||
return std::abs(animate::magnitude(target - value)) > config.epsilon;
|
||||
}
|
||||
|
||||
const auto error = value - target;
|
||||
const auto a_force = -config.k * error;
|
||||
const auto a_damping = -config.d * velocity;
|
||||
const auto a_total = a_force + a_damping;
|
||||
|
||||
velocity += a_total * dt;
|
||||
value += velocity * dt;
|
||||
|
||||
last_timestamp = now;
|
||||
|
||||
const bool done =
|
||||
animate::magnitude(error) < config.epsilon && std::abs(velocity) < config.epsilon;
|
||||
|
||||
if (done) velocity = animate::zero<T>();
|
||||
return !done;
|
||||
}
|
||||
};
|
||||
}
|
||||
86
modern-qt/utility/animation/transition.hh
Normal file
86
modern-qt/utility/animation/transition.hh
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "animatable.hh"
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <class T>
|
||||
concept transition_state_trait = requires(T& t) {
|
||||
typename T::ValueT;
|
||||
{ t.get_value() } -> std::same_as<typename T::ValueT>;
|
||||
{ t.get_target() } -> std::same_as<typename T::ValueT>;
|
||||
|
||||
{ t.set_value(std::declval<typename T::ValueT>()) };
|
||||
{ t.set_target(std::declval<typename T::ValueT>()) };
|
||||
|
||||
{ t.update() } -> std::same_as<bool>;
|
||||
};
|
||||
|
||||
// Functor like lambda
|
||||
template <transition_state_trait State>
|
||||
struct TransitionTask : public ITransitionTask {
|
||||
public:
|
||||
explicit TransitionTask(std::shared_ptr<State> state, std::shared_ptr<bool> token) noexcept
|
||||
: state { std::move(state) }
|
||||
, running { std::move(token) } { }
|
||||
|
||||
~TransitionTask() override = default;
|
||||
|
||||
auto update() noexcept -> bool override {
|
||||
return *running && state->update(); //
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<State> state;
|
||||
std::shared_ptr<bool> running;
|
||||
};
|
||||
|
||||
template <transition_state_trait State>
|
||||
struct TransitionValue {
|
||||
public:
|
||||
using T = State::ValueT;
|
||||
|
||||
explicit TransitionValue(Animatable& animatable, std::shared_ptr<State> state) noexcept
|
||||
: animatable { animatable }
|
||||
, state { std::move(state) } { }
|
||||
|
||||
auto get_state() const noexcept -> const State& { return *state; }
|
||||
|
||||
auto get_value() const noexcept { return state->get_value(); }
|
||||
|
||||
auto get_target() const noexcept { return state->get_target(); }
|
||||
|
||||
operator T() const noexcept { return state->get_value(); }
|
||||
|
||||
auto transition_to(const T& to) noexcept -> void {
|
||||
// Update target of state
|
||||
state->set_target(to);
|
||||
|
||||
// Clear last transition task
|
||||
if (running) {
|
||||
*running = false;
|
||||
}
|
||||
running = std::make_shared<bool>(true);
|
||||
|
||||
// Push new transition task
|
||||
auto task = std::make_unique<TransitionTask<State>>(state, running);
|
||||
animatable.push_transition_task(std::move(task));
|
||||
}
|
||||
auto snap_to(T to) noexcept -> void {
|
||||
state->set_value(std::move(to));
|
||||
state->set_target(std::move(to));
|
||||
if (running) *running = false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<State> state;
|
||||
std::shared_ptr<bool> running;
|
||||
|
||||
Animatable& animatable;
|
||||
};
|
||||
|
||||
template <transition_state_trait State>
|
||||
inline auto make_transition(Animatable& core, std::shared_ptr<State> state) {
|
||||
return std::make_unique<TransitionValue<State>>(core, state);
|
||||
}
|
||||
|
||||
}
|
||||
71
modern-qt/utility/animation/water-ripple.hh
Normal file
71
modern-qt/utility/animation/water-ripple.hh
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/animation/state/accessor.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPointF>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
struct WaterRippleState : public NormalAccessor {
|
||||
using ValueT = double;
|
||||
|
||||
QPointF origin;
|
||||
double value = 0.0;
|
||||
double target = 0.0;
|
||||
double speed = 1.0;
|
||||
|
||||
auto update() noexcept -> bool {
|
||||
value += speed;
|
||||
return value < target;
|
||||
}
|
||||
};
|
||||
|
||||
class WaterRippleRenderer {
|
||||
public:
|
||||
explicit WaterRippleRenderer(Animatable& core, double speed)
|
||||
: animatable { core }
|
||||
, speed { speed } { }
|
||||
|
||||
auto clicked(const QPointF& origin, double max_distance) noexcept -> void {
|
||||
auto state = std::make_shared<WaterRippleState>();
|
||||
state->origin = origin;
|
||||
state->speed = speed;
|
||||
state->target = max_distance;
|
||||
|
||||
auto ripple = make_transition(animatable, state);
|
||||
ripple->transition_to(max_distance);
|
||||
ripples.push_back(std::move(ripple));
|
||||
}
|
||||
|
||||
auto renderer(const QPainterPath& clip_path, const QColor& water_color) noexcept {
|
||||
return [&, this](QPainter& painter) {
|
||||
std::erase_if(ripples, [&](const auto& ripple) {
|
||||
const auto& state = ripple->get_state();
|
||||
const auto opacity = 1.0 - state.value / state.target;
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setClipPath(clip_path);
|
||||
painter.setOpacity(opacity);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(water_color);
|
||||
painter.drawEllipse(state.origin, state.value, state.value);
|
||||
painter.setOpacity(1.0);
|
||||
|
||||
return state.value >= state.target;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<TransitionValue<WaterRippleState>>> ripples;
|
||||
Animatable& animatable;
|
||||
double speed;
|
||||
};
|
||||
|
||||
}
|
||||
64
modern-qt/utility/content-scale.hh
Normal file
64
modern-qt/utility/content-scale.hh
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
#include <qdebug.h>
|
||||
#include <qpixmap.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
struct ContentScale {
|
||||
public:
|
||||
enum : uint8_t {
|
||||
NONE,
|
||||
FIT,
|
||||
CROP,
|
||||
FILL_WIDTH,
|
||||
FILL_HEIGHT,
|
||||
FILL_BOUNDS,
|
||||
INSIDE,
|
||||
} data;
|
||||
|
||||
operator decltype(data)() const noexcept { return data; }
|
||||
|
||||
auto transform(const QPixmap& pixmap, const QSize& size) const -> QPixmap {
|
||||
if (pixmap.isNull()) return {};
|
||||
|
||||
auto image_size = QPointF(pixmap.width(), pixmap.height());
|
||||
auto target_width = static_cast<double>(size.width());
|
||||
auto target_height = static_cast<double>(size.height());
|
||||
|
||||
constexpr auto mode = Qt::SmoothTransformation;
|
||||
|
||||
switch (data) {
|
||||
case ContentScale::NONE: {
|
||||
return pixmap;
|
||||
}
|
||||
case ContentScale::FIT: {
|
||||
image_size *= target_width / image_size.x();
|
||||
if (image_size.y() > target_height) image_size *= target_height / image_size.y();
|
||||
return pixmap.scaled(image_size.x(), image_size.y(), Qt::IgnoreAspectRatio, mode);
|
||||
}
|
||||
case ContentScale::CROP: {
|
||||
image_size *= target_width / image_size.x();
|
||||
if (image_size.y() < target_height) image_size *= target_height / image_size.y();
|
||||
return pixmap.scaled(image_size.x(), image_size.y(), Qt::IgnoreAspectRatio, mode);
|
||||
}
|
||||
case ContentScale::INSIDE: {
|
||||
if (image_size.x() > target_width) image_size *= target_width / image_size.x();
|
||||
if (image_size.y() > target_height) image_size *= target_height / image_size.y();
|
||||
return pixmap.scaled(image_size.x(), image_size.y(), Qt::IgnoreAspectRatio, mode);
|
||||
}
|
||||
case ContentScale::FILL_BOUNDS: {
|
||||
return pixmap.scaled(size, Qt::IgnoreAspectRatio, mode);
|
||||
}
|
||||
case ContentScale::FILL_WIDTH:
|
||||
return pixmap.scaled(target_width, image_size.y() * target_width / image_size.x(),
|
||||
Qt::IgnoreAspectRatio, mode);
|
||||
case ContentScale::FILL_HEIGHT:
|
||||
return pixmap.scaled(image_size.x() * target_height / image_size.y(), target_height,
|
||||
Qt::IgnoreAspectRatio, mode);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
166
modern-qt/utility/material-icon.hh
Normal file
166
modern-qt/utility/material-icon.hh
Normal file
@@ -0,0 +1,166 @@
|
||||
#pragma once
|
||||
|
||||
#include <qfont.h>
|
||||
#include <qfontdatabase.h>
|
||||
|
||||
namespace creeper {
|
||||
namespace material {
|
||||
|
||||
namespace size {
|
||||
constexpr auto _0 = int { 15 };
|
||||
constexpr auto _1 = int { 18 };
|
||||
constexpr auto _2 = int { 18 };
|
||||
constexpr auto _3 = int { 24 };
|
||||
constexpr auto _4 = int { 32 };
|
||||
}
|
||||
class FontLoader {
|
||||
// static inline QString sharp_font_name = "";
|
||||
// static inline QString round_font_name = "";
|
||||
// static inline QString outline_font_name = "";
|
||||
static inline QString material_font = "";
|
||||
|
||||
public:
|
||||
static void load_font() {
|
||||
int fontId = QFontDatabase::addApplicationFont(":/ttf/ttf/MaterialIcons-Regular.ttf");
|
||||
material_font = get_font_family(fontId, "Material Icons");
|
||||
}
|
||||
private:
|
||||
static QString get_font_family(int fontId, const QString& fallback) {
|
||||
if (fontId == -1) {
|
||||
qWarning() << "Failed to load font:" << fallback;
|
||||
return fallback;
|
||||
}
|
||||
QStringList families = QFontDatabase::applicationFontFamilies(fontId);
|
||||
if (families.isEmpty()) {
|
||||
qWarning() << "No families found for font:" << fallback;
|
||||
return fallback;
|
||||
}
|
||||
qDebug() << "families found for font:" << families;
|
||||
return families.first();
|
||||
}
|
||||
};
|
||||
namespace sharp {
|
||||
constexpr auto font = "Material Icons Sharp";
|
||||
inline const auto font_0 = QFont { font, size::_0 };
|
||||
inline const auto font_1 = QFont { font, size::_1 };
|
||||
inline const auto font_2 = QFont { font, size::_2 };
|
||||
inline const auto font_3 = QFont { font, size::_3 };
|
||||
inline const auto font_4 = QFont { font, size::_4 };
|
||||
}
|
||||
namespace round {
|
||||
constexpr auto font = "Material Icons Round";
|
||||
inline const auto font_0 = QFont { font, size::_0 };
|
||||
inline const auto font_1 = QFont { font, size::_1 };
|
||||
inline const auto font_2 = QFont { font, size::_2 };
|
||||
inline const auto font_3 = QFont { font, size::_3 };
|
||||
inline const auto font_4 = QFont { font, size::_4 };
|
||||
}
|
||||
namespace outlined {
|
||||
constexpr auto font = "Material Icons Outlined";
|
||||
inline const auto font_0 = QFont { font, size::_0 };
|
||||
inline const auto font_1 = QFont { font, size::_1 };
|
||||
inline const auto font_2 = QFont { font, size::_2 };
|
||||
inline const auto font_3 = QFont { font, size::_3 };
|
||||
inline const auto font_4 = QFont { font, size::_4 };
|
||||
}
|
||||
namespace regular {
|
||||
constexpr auto font = "Material Icons";
|
||||
inline const auto font_0 = QFont { font, size::_0 };
|
||||
inline const auto font_1 = QFont { font, size::_1 };
|
||||
inline const auto font_2 = QFont { font, size::_2 };
|
||||
inline const auto font_3 = QFont { font, size::_3 };
|
||||
inline const auto font_4 = QFont { font, size::_4 };
|
||||
}
|
||||
|
||||
constexpr auto kFontSizeExtraSmall = size::_0;
|
||||
constexpr auto kFontSizeSmall = size::_1;
|
||||
constexpr auto kFontSizeMedium = size::_2;
|
||||
constexpr auto kFontSizeLarge = size::_3;
|
||||
constexpr auto kFontSizeExtraLarge = size::_4;
|
||||
|
||||
constexpr auto kSharpFontName = sharp::font;
|
||||
inline const auto kSharpExtraSmallFont = sharp::font_0;
|
||||
inline const auto kSharpSmallFont = sharp::font_1;
|
||||
inline const auto kSharpMediumFont = sharp::font_2;
|
||||
inline const auto kSharpLargeFont = sharp::font_3;
|
||||
inline const auto kSharpExtraLargeFont = sharp::font_4;
|
||||
|
||||
constexpr auto kRoundFontName = round::font;
|
||||
inline const auto kRoundExtraSmallFont = round::font_0;
|
||||
inline const auto kRoundSmallFont = round::font_1;
|
||||
inline const auto kRoundMediumFont = round::font_2;
|
||||
inline const auto kRoundLargeFont = round::font_3;
|
||||
inline const auto kRoundExtraLargeFont = round::font_4;
|
||||
|
||||
constexpr auto kOutlinedFontName = outlined::font;
|
||||
inline const auto kOutlinedExtraSmallFont = outlined::font_0;
|
||||
inline const auto kOutlinedSmallFont = outlined::font_1;
|
||||
inline const auto kOutlinedMediumFont = outlined::font_2;
|
||||
inline const auto kOutlinedLargeFont = outlined::font_3;
|
||||
inline const auto kOutlinedExtraLargeFont = outlined::font_4;
|
||||
|
||||
namespace icon {
|
||||
|
||||
// Function
|
||||
constexpr auto kSettings = "settings";
|
||||
constexpr auto kSearch = "search";
|
||||
constexpr auto kHome = "home";
|
||||
constexpr auto kMenu = "menu";
|
||||
constexpr auto kInfo = "info";
|
||||
constexpr auto kHelp = "help";
|
||||
constexpr auto kRefresh = "refresh";
|
||||
constexpr auto kMoreVert = "more_vert";
|
||||
constexpr auto kMoreHoriz = "more_horiz";
|
||||
constexpr auto kNotifications = "notifications";
|
||||
constexpr auto kDashboard = "dashboard";
|
||||
constexpr auto kExtension = "extension";
|
||||
|
||||
// Shape
|
||||
constexpr auto kFavorite = "favorite";
|
||||
constexpr auto kStar = "star";
|
||||
constexpr auto kHeartBroken = "heart_broken";
|
||||
constexpr auto kCheck = "check";
|
||||
constexpr auto kCircle = "circle";
|
||||
constexpr auto kSquare = "square";
|
||||
constexpr auto kArrowUp = "arrow_upward";
|
||||
constexpr auto kArrowDown = "arrow_downward";
|
||||
constexpr auto kArrowLeft = "arrow_back";
|
||||
constexpr auto kArrowRight = "arrow_forward";
|
||||
|
||||
// Action
|
||||
constexpr auto kClose = "close";
|
||||
constexpr auto kAdd = "add";
|
||||
constexpr auto kEdit = "edit";
|
||||
constexpr auto kDelete = "delete";
|
||||
constexpr auto kSave = "save";
|
||||
constexpr auto kShare = "share";
|
||||
constexpr auto kSend = "send";
|
||||
constexpr auto kUpload = "upload";
|
||||
constexpr auto kDownload = "download";
|
||||
constexpr auto kCheckCircle = "check_circle";
|
||||
constexpr auto kCancel = "cancel";
|
||||
constexpr auto kOpenInNew = "open_in_new";
|
||||
constexpr auto kLogout = "logout";
|
||||
constexpr auto kRoutine = "routine";
|
||||
constexpr auto kDarkMode = "dark_mode";
|
||||
|
||||
// File
|
||||
constexpr auto kFolder = "folder";
|
||||
constexpr auto kFolderOpen = "folder_open";
|
||||
constexpr auto kInsertDrive = "insert_drive_file";
|
||||
constexpr auto kAttachFile = "attach_file";
|
||||
constexpr auto kCloud = "cloud";
|
||||
constexpr auto kCloudDownload = "cloud_download";
|
||||
constexpr auto kCloudUpload = "cloud_upload";
|
||||
constexpr auto kFileCopy = "file_copy";
|
||||
constexpr auto kDescription = "description";
|
||||
|
||||
// combobox
|
||||
constexpr auto kArrowDropDown = "arrow_drop_down";
|
||||
|
||||
// link
|
||||
constexpr auto kAddLink = "add_link";
|
||||
constexpr auto kLinkOff = "link_off";
|
||||
}
|
||||
}
|
||||
}
|
||||
122
modern-qt/utility/painter-resource.hh
Normal file
122
modern-qt/utility/painter-resource.hh
Normal file
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
#include <qnetworkaccessmanager.h>
|
||||
#include <qnetworkreply.h>
|
||||
#include <qpixmap.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
namespace painter_resource {
|
||||
template <typename T>
|
||||
concept finished_callback_c = std::invocable<T> || std::invocable<T, QPixmap&>;
|
||||
}
|
||||
|
||||
struct PainterResource : public QPixmap {
|
||||
|
||||
constexpr explicit PainterResource(std::string_view url) noexcept
|
||||
: QPixmap {} {
|
||||
const auto qurl = QUrl(QString::fromUtf8(url.data(), static_cast<int>(url.size())));
|
||||
if (is_filesystem_url(url) || is_qt_resource_url(url)) {
|
||||
qDebug() << "[PainterResource] is_filesystem_url" << url;
|
||||
QPixmap::load(qurl.path());
|
||||
} else if (is_network_url(url)) {
|
||||
download_resource_from_network(qurl, [](auto&) { });
|
||||
} else {
|
||||
qWarning() << "[PainterResource] Failed to recognize the type of url";
|
||||
}
|
||||
}
|
||||
constexpr explicit PainterResource(std::string_view url, auto&& f) noexcept
|
||||
requires painter_resource::finished_callback_c<decltype(f)>
|
||||
{
|
||||
const auto qurl = QUrl(QString::fromUtf8(url.data(), static_cast<int>(url.size())));
|
||||
if (is_network_url(url)) {
|
||||
download_resource_from_network(qurl, f);
|
||||
} else {
|
||||
qWarning() << "[PainterResource] Only network url can be used with callback";
|
||||
}
|
||||
}
|
||||
|
||||
~PainterResource() noexcept { *resource_exiting = false; }
|
||||
|
||||
template <typename T>
|
||||
explicit PainterResource(T&& other) noexcept
|
||||
requires std::convertible_to<T, QPixmap>
|
||||
: QPixmap(std::forward<T>(other)) { }
|
||||
|
||||
template <typename T>
|
||||
auto operator=(T&& other) noexcept -> PainterResource&
|
||||
requires std::convertible_to<T, QPixmap>
|
||||
{
|
||||
QPixmap::operator=(std::forward<T>(other));
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto is_loading() const noexcept -> bool { return is_loading_; }
|
||||
|
||||
auto is_error() const noexcept -> bool { return is_error_; }
|
||||
|
||||
auto add_finished_callback(std::invocable<PainterResource&> auto&& f) {
|
||||
finished_callback_ = std::forward<decltype(f)>(f);
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::function<void(PainterResource&)>> finished_callback_;
|
||||
|
||||
bool is_loading_ = false;
|
||||
bool is_error_ = false;
|
||||
|
||||
std::shared_ptr<bool> resource_exiting = std::make_shared<bool>(true);
|
||||
|
||||
auto download_resource_from_network(const QUrl& url, auto&& f) noexcept -> void
|
||||
requires painter_resource::finished_callback_c<decltype(f)>
|
||||
{
|
||||
is_loading_ = true;
|
||||
|
||||
auto manager = new QNetworkAccessManager;
|
||||
auto replay = manager->get(QNetworkRequest { url });
|
||||
|
||||
auto resource_exiting = this->resource_exiting;
|
||||
QObject::connect(replay, &QNetworkReply::finished, [=, this] {
|
||||
if (!*resource_exiting) {
|
||||
qWarning() << "[PainterResource] Async task aborted: "
|
||||
"Resource instance has been destroyed.";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto error = replay->error();
|
||||
const auto data = replay->readAll();
|
||||
if (error != QNetworkReply::NoError) {
|
||||
is_error_ = true;
|
||||
qWarning() << "[PainterResource] Network error:" << replay->errorString();
|
||||
} else if (data.isNull()) {
|
||||
is_error_ = true;
|
||||
} else {
|
||||
is_error_ = false;
|
||||
loadFromData(data);
|
||||
}
|
||||
is_loading_ = false;
|
||||
manager->deleteLater();
|
||||
|
||||
using F = decltype(f);
|
||||
if constexpr (std::invocable<F, PainterResource&>) std::invoke(f, *this);
|
||||
if constexpr (std::invocable<F>) std::invoke(f);
|
||||
|
||||
if (finished_callback_) std::invoke(*finished_callback_, *this);
|
||||
});
|
||||
}
|
||||
|
||||
static constexpr auto starts_with(std::string_view s, std::string_view prefix) -> bool {
|
||||
return s.substr(0, prefix.size()) == prefix;
|
||||
}
|
||||
static constexpr auto is_filesystem_url(std::string_view url) -> bool {
|
||||
return !starts_with(url, "http://") && !starts_with(url, "https://")
|
||||
&& !starts_with(url, "qrc:/") && !starts_with(url, ":/");
|
||||
}
|
||||
static constexpr auto is_qt_resource_url(std::string_view url) -> bool {
|
||||
return starts_with(url, "qrc:/") || starts_with(url, ":/");
|
||||
}
|
||||
static constexpr auto is_network_url(std::string_view url) -> bool {
|
||||
return starts_with(url, "http://") || starts_with(url, "https://");
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
98
modern-qt/utility/painter/common.hh
Normal file
98
modern-qt/utility/painter/common.hh
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
|
||||
#include <qpainter.h>
|
||||
|
||||
namespace creeper::qt {
|
||||
using painter = QPainter;
|
||||
using point = QPointF;
|
||||
using size = QSizeF;
|
||||
using rect = QRectF;
|
||||
using color = QColor;
|
||||
using real = qreal;
|
||||
using align = Qt::Alignment;
|
||||
using string = QString;
|
||||
using font = QFont;
|
||||
using text_option = QTextOption;
|
||||
}
|
||||
|
||||
namespace creeper::painter {
|
||||
|
||||
template <class T>
|
||||
concept common_trait = requires(T t) {
|
||||
{ auto { t.origin } } -> std::same_as<qt::point>;
|
||||
{ auto { t.size } } -> std::same_as<qt::size>;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept container_trait = requires(T t) {
|
||||
{ auto { t.align } } -> std::same_as<qt::align>;
|
||||
} && common_trait<T>;
|
||||
|
||||
template <class T>
|
||||
concept shape_trait = requires(T t) {
|
||||
{ auto { t.color_container } } -> std::same_as<qt::color>;
|
||||
{ auto { t.color_outline } } -> std::same_as<qt::color>;
|
||||
{ auto { t.thickness_outline } } -> std::same_as<qt::real>;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept drawable_trait = common_trait<T> && std::invocable<T, qt::painter&>;
|
||||
|
||||
struct CommonProps {
|
||||
qt::point origin = qt::point { 0, 0 };
|
||||
qt::size size = qt::size { 0, 0 };
|
||||
auto rect() const { return qt::rect { origin, size }; }
|
||||
};
|
||||
|
||||
struct ContainerProps {
|
||||
qt::size size = qt::size { 0, 0 };
|
||||
qt::align align = qt::align {};
|
||||
qt::point origin = qt::point { 0, 0 };
|
||||
auto rect() const { return qt::rect { origin, size }; }
|
||||
};
|
||||
|
||||
struct ShapeProps {
|
||||
qt::color container_color = Qt::transparent;
|
||||
qt::color outline_color = Qt::transparent;
|
||||
qt::real outline_width = 0;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::painter::common::pro {
|
||||
|
||||
struct Token { };
|
||||
|
||||
using Size = DerivedProp<Token, qt::size, [](auto& self, auto const& v) { self.size = v; }>;
|
||||
using Origin = DerivedProp<Token, qt::point, [](auto& self, auto const& v) { self.origin = v; }>;
|
||||
|
||||
using ContainerColor =
|
||||
SetterProp<Token, qt::color, [](auto& self, auto const& v) { self.container_color = v; }>;
|
||||
using OutlineColor =
|
||||
SetterProp<Token, qt::color, [](auto& self, auto const& v) { self.outline_color = v; }>;
|
||||
using OutlineWidth =
|
||||
SetterProp<Token, qt::real, [](auto& self, auto const& v) { self.outline_width = v; }>;
|
||||
|
||||
struct Outline : Token {
|
||||
qt::color color;
|
||||
qt::real width;
|
||||
Outline(const qt::color& color, qt::real width)
|
||||
: color { color }
|
||||
, width { width } { }
|
||||
auto apply(auto& self) {
|
||||
self.outline_color = color;
|
||||
self.outline_width = width;
|
||||
}
|
||||
};
|
||||
|
||||
/// Alias
|
||||
|
||||
using Fill = ContainerColor;
|
||||
|
||||
/// Export
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
}
|
||||
347
modern-qt/utility/painter/container.hh
Normal file
347
modern-qt/utility/painter/container.hh
Normal file
@@ -0,0 +1,347 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/painter/common.hh"
|
||||
|
||||
namespace creeper::painter {
|
||||
|
||||
// 核心容器结构体,现在继承自 Impl,使其满足 drawable_trait (假设 Impl 继承了所需的属性)
|
||||
template <class Impl, drawable_trait... Ts>
|
||||
struct Container : public Impl {
|
||||
std::tuple<std::decay_t<Ts>...> drawable;
|
||||
|
||||
// 唯一构造函数:接受 Impl 实例和可变参数包
|
||||
constexpr explicit Container(const Impl& impl, Ts&&... drawable)
|
||||
: Impl { impl }
|
||||
, drawable { std::make_tuple(std::forward<Ts>(drawable)...) } { }
|
||||
|
||||
auto operator()(qt::painter& painter)
|
||||
requires(std::invocable<Ts, qt::painter&> && ...)
|
||||
{
|
||||
render(painter);
|
||||
}
|
||||
auto render(qt::painter& painter) noexcept
|
||||
requires(std::invocable<Ts, qt::painter&> && ...)
|
||||
{
|
||||
constexpr auto has_unique = requires { //
|
||||
static_cast<Impl&>(*this).unique_render(painter, drawable);
|
||||
};
|
||||
if constexpr (has_unique) {
|
||||
static_cast<Impl&>(*this).unique_render(painter, drawable);
|
||||
} else {
|
||||
Impl::make_layout(drawable);
|
||||
|
||||
auto f = [&](auto&... d) { (d(painter), ...); };
|
||||
std::apply(std::move(f), drawable);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 布局实现基类
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
struct MakeLayoutFunction {
|
||||
template <drawable_trait... Ts>
|
||||
auto make_layout(this auto& self, std::tuple<Ts...>& drawable) {
|
||||
std::apply([&self](auto&... d) { ((self.make(d)), ...); }, drawable);
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// SurfaceImpl (仅平移)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
struct SurfaceImpl : public MakeLayoutFunction, ContainerProps {
|
||||
constexpr explicit SurfaceImpl(const qt::size& size, const qt::point& origin = {})
|
||||
: ContainerProps {
|
||||
.size = size,
|
||||
.origin = origin,
|
||||
} { }
|
||||
auto make(drawable_trait auto& drawable) {
|
||||
const auto& container_origin = origin;
|
||||
|
||||
auto& drawable_origin = drawable.origin;
|
||||
drawable_origin.setX(container_origin.x() + drawable_origin.x());
|
||||
drawable_origin.setY(container_origin.y() + drawable_origin.y());
|
||||
};
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// BufferImpl
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
struct BufferImpl : public MakeLayoutFunction, ContainerProps {
|
||||
mutable QPixmap buffer;
|
||||
|
||||
constexpr explicit BufferImpl(const qt::size& size, const qt::point& origin = { 0, 0 })
|
||||
: ContainerProps {
|
||||
.size = size,
|
||||
.origin = origin,
|
||||
} { }
|
||||
|
||||
auto make(drawable_trait auto& drawable) {
|
||||
const auto& container_origin = origin;
|
||||
drawable.origin.setX(container_origin.x() + drawable.origin.x());
|
||||
drawable.origin.setY(container_origin.y() + drawable.origin.y());
|
||||
};
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
auto unique_render(qt::painter& main_painter, std::tuple<Ts...>& drawable) noexcept {
|
||||
make_layout(drawable);
|
||||
|
||||
if (buffer.size() != size || buffer.isNull()) {
|
||||
buffer = QPixmap(size.width(), size.height());
|
||||
buffer.fill(Qt::transparent);
|
||||
}
|
||||
buffer.fill(Qt::transparent);
|
||||
|
||||
auto buffer_painter = qt::painter { &buffer };
|
||||
buffer_painter.translate(-origin.x(), -origin.y());
|
||||
|
||||
const auto f = [&](auto&... args) {
|
||||
(
|
||||
[&]() {
|
||||
buffer_painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
args(buffer_painter);
|
||||
}(),
|
||||
...);
|
||||
};
|
||||
std::apply(std::move(f), drawable);
|
||||
buffer_painter.end();
|
||||
|
||||
main_painter.drawPixmap(origin, buffer);
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// BoxImpl (居中对齐)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
struct BoxImpl : public MakeLayoutFunction, ContainerProps {
|
||||
|
||||
constexpr explicit BoxImpl(
|
||||
const qt::size& size, const qt::align& align, const qt::point& origin = {})
|
||||
: ContainerProps {
|
||||
.size = size,
|
||||
.align = align,
|
||||
.origin = origin,
|
||||
} { }
|
||||
|
||||
auto make(drawable_trait auto& drawable) {
|
||||
const auto container_align = align;
|
||||
const auto container_size = size;
|
||||
const auto container_origin = origin;
|
||||
|
||||
auto& drawable_origin = drawable.origin;
|
||||
auto& drawable_size = drawable.size;
|
||||
|
||||
const auto container_w = container_size.width();
|
||||
const auto container_h = container_size.height();
|
||||
|
||||
if (container_align & Qt::AlignRight) {
|
||||
drawable_origin.setX(container_origin.x() + container_w - drawable_size.width());
|
||||
} else if (container_align & Qt::AlignHCenter) {
|
||||
const auto dx = (container_w - drawable_size.width()) / 2;
|
||||
drawable_origin.setX(container_origin.x() + dx);
|
||||
} else {
|
||||
drawable_origin.setX(container_origin.x());
|
||||
}
|
||||
|
||||
if (container_align & Qt::AlignBottom) {
|
||||
drawable_origin.setY(container_origin.y() + container_h - drawable_size.height());
|
||||
} else if (container_align & Qt::AlignVCenter) {
|
||||
const auto dy = (container_h - drawable_size.height()) / 2;
|
||||
drawable_origin.setY(container_origin.y() + dy);
|
||||
} else {
|
||||
drawable_origin.setY(container_origin.y());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// RowImpl (横向流式布局)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
struct RowImpl : public MakeLayoutFunction, ContainerProps {
|
||||
// 主轴对齐 (Horizontal)
|
||||
const qt::align main_align;
|
||||
|
||||
constexpr explicit RowImpl(
|
||||
const qt::size& size,
|
||||
const qt::align& main_align = Qt::AlignLeft, // 主轴对齐:AlignLeft/AlignRight/AlignHCenter
|
||||
const qt::align& cross_align = Qt::AlignVCenter, // 非主轴对齐:AlignTop/AlignBottom/AlignVCenter
|
||||
const qt::point& origin = {})
|
||||
: ContainerProps {
|
||||
.size = size,
|
||||
.align = cross_align, // ContainerProps::align 存储非主轴对齐
|
||||
.origin = origin,
|
||||
}
|
||||
, main_align(main_align) // 存储主轴对齐
|
||||
{ }
|
||||
|
||||
mutable int current_x = 0;
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
auto make_layout(this auto& self, std::tuple<Ts...>& drawable) {
|
||||
// 1. 计算主轴总尺寸 (Total Width)
|
||||
int total_width = 0;
|
||||
std::apply(
|
||||
[&total_width](auto&... d) { ((total_width += d.size.width()), ...); }, drawable);
|
||||
|
||||
// 2. 计算主轴偏移 (Main Axis Offset)
|
||||
int initial_x_offset = 0;
|
||||
const int remaining_space = self.size.width() - total_width;
|
||||
|
||||
if (remaining_space > 0) {
|
||||
if (self.main_align & Qt::AlignRight) {
|
||||
initial_x_offset = remaining_space;
|
||||
} else if (self.main_align & Qt::AlignHCenter) {
|
||||
initial_x_offset = remaining_space / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置起始 X 坐标
|
||||
self.current_x = self.origin.x() + initial_x_offset;
|
||||
|
||||
// 4. 应用布局到每个元素
|
||||
std::apply([&self](auto&... d) { ((self.make(d)), ...); }, drawable);
|
||||
}
|
||||
|
||||
auto make(drawable_trait auto& drawable) {
|
||||
const auto container_cross_align = align; // 非主轴对齐 (垂直)
|
||||
const auto container_size = size;
|
||||
const auto container_origin = origin;
|
||||
|
||||
auto& drawable_origin = drawable.origin;
|
||||
const auto drawable_h = drawable.size.height();
|
||||
|
||||
// 1. 主轴布局 (X 坐标累加)
|
||||
drawable_origin.setX(current_x);
|
||||
current_x += drawable.size.width();
|
||||
|
||||
// 2. 非主轴对齐 (垂直对齐)
|
||||
if (container_cross_align & Qt::AlignBottom) {
|
||||
drawable_origin.setY(container_origin.y() + container_size.height() - drawable_h);
|
||||
} else if (container_cross_align & Qt::AlignVCenter) {
|
||||
const auto dy = (container_size.height() - drawable_h) / 2;
|
||||
drawable_origin.setY(container_origin.y() + dy);
|
||||
} else { // 默认 AlignTop
|
||||
drawable_origin.setY(container_origin.y());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// ColImpl (垂直流式布局)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
struct ColImpl : public MakeLayoutFunction, ContainerProps {
|
||||
// 主轴对齐 (Vertical)
|
||||
const qt::align main_align;
|
||||
|
||||
constexpr explicit ColImpl(
|
||||
const qt::size& size,
|
||||
const qt::align& main_align = Qt::AlignTop, // 主轴对齐:AlignTop/AlignBottom/AlignVCenter
|
||||
const qt::align& cross_align = Qt::AlignHCenter, // 非主轴对齐:AlignLeft/AlignRight/AlignHCenter
|
||||
const qt::point& origin = {})
|
||||
: ContainerProps {
|
||||
.size = size,
|
||||
.align = cross_align, // ContainerProps::align 存储非主轴对齐
|
||||
.origin = origin,
|
||||
}
|
||||
, main_align(main_align) // 存储主轴对齐
|
||||
{ }
|
||||
|
||||
mutable int current_y = 0;
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
auto make_layout(this auto& self, std::tuple<Ts...>& drawable) {
|
||||
// 1. 计算主轴总尺寸 (Total Height)
|
||||
int total_height = 0;
|
||||
std::apply(
|
||||
[&total_height](auto&... d) { ((total_height += d.size.height()), ...); }, drawable);
|
||||
|
||||
// 2. 计算主轴偏移 (Main Axis Offset)
|
||||
int initial_y_offset = 0;
|
||||
const int remaining_space = self.size.height() - total_height;
|
||||
|
||||
if (remaining_space > 0) {
|
||||
if (self.main_align & Qt::AlignBottom) {
|
||||
initial_y_offset = remaining_space;
|
||||
} else if (self.main_align & Qt::AlignVCenter) {
|
||||
initial_y_offset = remaining_space / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置起始 Y 坐标
|
||||
self.current_y = self.origin.y() + initial_y_offset;
|
||||
|
||||
// 4. 应用布局到每个元素
|
||||
std::apply([&self](auto&... d) { ((self.make(d)), ...); }, drawable);
|
||||
}
|
||||
|
||||
auto make(drawable_trait auto& drawable) {
|
||||
const auto container_cross_align = align; // 非主轴对齐 (水平)
|
||||
const auto container_size = size;
|
||||
const auto container_origin = origin;
|
||||
|
||||
auto& drawable_origin = drawable.origin;
|
||||
const auto drawable_w = drawable.size.width();
|
||||
|
||||
// 1. 主轴布局 (Y 坐标累加)
|
||||
drawable_origin.setY(current_y);
|
||||
current_y += drawable.size.height();
|
||||
|
||||
// 2. 非主轴对齐 (水平对齐)
|
||||
if (container_cross_align & Qt::AlignRight) {
|
||||
drawable_origin.setX(container_origin.x() + container_size.width() - drawable_w);
|
||||
} else if (container_cross_align & Qt::AlignHCenter) {
|
||||
const auto dx = (container_size.width() - drawable_w) / 2;
|
||||
drawable_origin.setX(container_origin.x() + dx);
|
||||
} else { // 默认 AlignLeft
|
||||
drawable_origin.setX(container_origin.x());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 通用 Container 推导指引 (用于简化用户代码)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
template <typename... Ts>
|
||||
Container(const SurfaceImpl& impl, Ts&&... args) -> Container<SurfaceImpl, Ts...>;
|
||||
|
||||
template <typename... Ts>
|
||||
Container(const BufferImpl& impl, Ts&&... args) -> Container<BufferImpl, Ts...>;
|
||||
|
||||
template <typename... Ts>
|
||||
Container(const BoxImpl& impl, Ts&&... args) -> Container<BoxImpl, Ts...>;
|
||||
|
||||
template <typename... Ts>
|
||||
Container(const RowImpl& impl, Ts&&... args) -> Container<RowImpl, Ts...>;
|
||||
|
||||
template <typename... Ts>
|
||||
Container(const ColImpl& impl, Ts&&... args) -> Container<ColImpl, Ts...>;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Paint 类型导出
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
namespace Paint {
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
using Surface = Container<SurfaceImpl, Ts...>;
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
using Buffer = Container<BufferImpl, Ts...>;
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
using Box = Container<BoxImpl, Ts...>;
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
using Row = Container<RowImpl, Ts...>;
|
||||
|
||||
template <drawable_trait... Ts>
|
||||
using Col = Container<ColImpl, Ts...>;
|
||||
}
|
||||
|
||||
} // namespace creeper::painter
|
||||
15
modern-qt/utility/painter/helper.cc
Normal file
15
modern-qt/utility/painter/helper.cc
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "modern-qt/utility/painter/helper.hh"
|
||||
#include <qdebug.h>
|
||||
|
||||
namespace creeper::util {
|
||||
|
||||
constexpr auto enable_print_paint_event_count = bool { false };
|
||||
|
||||
auto print_paint_event_count() noexcept -> void {
|
||||
if constexpr (enable_print_paint_event_count) {
|
||||
static auto count = std::size_t { 0 };
|
||||
qDebug() << "[PainterHelper] Paint Event:" << count++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
224
modern-qt/utility/painter/helper.hh
Normal file
224
modern-qt/utility/painter/helper.hh
Normal file
@@ -0,0 +1,224 @@
|
||||
#pragma once
|
||||
|
||||
#include <qpainter.h>
|
||||
#include <qpainterpath.h>
|
||||
|
||||
namespace creeper::util {
|
||||
|
||||
auto print_paint_event_count() noexcept -> void;
|
||||
|
||||
/// @brief 隐藏冗杂的细节,解放命令式的绘图调用
|
||||
class PainterHelper {
|
||||
public:
|
||||
using Renderer = std::function<void(QPainter&)>;
|
||||
|
||||
explicit PainterHelper(QPainter& painter)
|
||||
: painter(painter) {
|
||||
print_paint_event_count();
|
||||
}
|
||||
|
||||
inline void done() { }
|
||||
|
||||
inline PainterHelper& apply(const Renderer& renderer) {
|
||||
renderer(painter);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& apply(const std::ranges::range auto& renderers)
|
||||
requires std::same_as<std::ranges::range_value_t<decltype(renderers)>, Renderer>
|
||||
{
|
||||
for (const auto& renderer : renderers)
|
||||
renderer(painter);
|
||||
return *this;
|
||||
}
|
||||
|
||||
public:
|
||||
inline PainterHelper& set_brush(const QBrush& brush) {
|
||||
painter.setBrush(brush);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& set_pen(const QPen& pen) {
|
||||
painter.setPen(pen);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& set_opacity(double opacity) {
|
||||
painter.setOpacity(opacity);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& set_render_hint(QPainter::RenderHint hint, bool on = true) {
|
||||
painter.setRenderHint(hint, on);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& set_render_hints(QPainter::RenderHints hint, bool on = true) {
|
||||
painter.setRenderHints(hint, on);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& set_clip_path(
|
||||
const QPainterPath& path, Qt::ClipOperation operation = Qt::ReplaceClip) {
|
||||
painter.setClipPath(path, operation);
|
||||
return *this;
|
||||
}
|
||||
|
||||
public:
|
||||
inline PainterHelper& point(const QColor& color, double radius, const QPointF& point) {
|
||||
pen_only({ color, radius * 2 }).drawPoint(point);
|
||||
return *this;
|
||||
}
|
||||
inline PainterHelper& ellipse(const QColor& background, const QColor& border_color,
|
||||
double border_width, const QRectF& rect) {
|
||||
|
||||
brush_only({ background }).drawEllipse(rect);
|
||||
|
||||
const auto half = border_width / 2;
|
||||
if (border_width != 0)
|
||||
pen_only({ border_color, border_width })
|
||||
.drawEllipse(rect.adjusted(half, half, -half, -half));
|
||||
|
||||
return *this;
|
||||
}
|
||||
inline PainterHelper& ellipse(const QColor& background, const QColor& border_color,
|
||||
double border_width, const QPointF& origin, double radius_x, double radius_y) {
|
||||
|
||||
brush_only({ background }).drawEllipse(origin, radius_x, radius_y);
|
||||
|
||||
if (border_width != 0)
|
||||
pen_only({ border_color, border_width })
|
||||
.drawEllipse(origin, radius_x - border_width / 2, radius_y - border_width / 2);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& rectangle(const QColor& background, const QColor& border_color,
|
||||
double border_width, const QRectF& rect) {
|
||||
|
||||
brush_only({ background }).drawRect(rect);
|
||||
|
||||
if (border_width == 0) return *this;
|
||||
|
||||
const auto inliner_border_rectangle =
|
||||
rect.adjusted(border_width / 2, border_width / 2, -border_width / 2, -border_width / 2);
|
||||
|
||||
pen_only({ border_color, border_width }).drawRect(inliner_border_rectangle);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& rounded_rectangle(const QColor& background, const QColor& border_color,
|
||||
double border_width, const QRectF& rect, double radius_x, double radius_y) {
|
||||
|
||||
brush_only({ background }).drawRoundedRect(rect, radius_x, radius_y);
|
||||
|
||||
if (border_width == 0) return *this;
|
||||
const auto inliner_border_rectangle =
|
||||
rect.adjusted(border_width / 2, border_width / 2, -border_width / 2, -border_width / 2);
|
||||
|
||||
pen_only({ border_color, border_width })
|
||||
.drawRoundedRect(inliner_border_rectangle, std::max(radius_x - border_width / 2, 0.),
|
||||
std::max(radius_y - border_width / 2, 0.));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& rounded_rectangle(const QColor& background, const QColor& border_color,
|
||||
double border_width, const QRectF& rect, double tl, double tr, double br, double bl) {
|
||||
|
||||
const auto path = make_rounded_rect_path(rect, tl, tr, br, bl);
|
||||
brush_only({ background }).drawPath(path);
|
||||
|
||||
if (border_width == 0) return *this;
|
||||
|
||||
const auto inliner = [=](double r) { return std::max(r - border_width / 2, 0.); };
|
||||
const auto inliner_border_rectangle =
|
||||
rect.adjusted(border_width / 2, border_width / 2, -border_width / 2, -border_width / 2);
|
||||
const auto inliner_path = make_rounded_rect_path(
|
||||
inliner_border_rectangle, inliner(tl), inliner(tr), inliner(br), inliner(bl));
|
||||
|
||||
pen_only({ border_color, border_width }).drawPath(inliner_path);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Pen 是以路径为中心来绘制图片,有绘出 rect 导致画面被裁切的可能,由于是 path 类型,不好做限制
|
||||
inline PainterHelper& path(const QColor& background, const QColor& border_color,
|
||||
double border_width, const QPainterPath& path) {
|
||||
brush_only({ background }).drawPath(path);
|
||||
if (border_width != 0) pen_only({ border_color, border_width }).drawPath(path);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& pixmap(const QPixmap& pixmap, const QRectF& dst, const QRectF& src) {
|
||||
painter.drawPixmap(dst, pixmap, src);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PainterHelper& simple_text(const QString& text, const QFont& font, const QColor& color,
|
||||
const QRectF& rect, Qt::Alignment alignment) {
|
||||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||||
painter.setFont(font);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen({ color });
|
||||
painter.drawText(rect, alignment, text);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
QPainter& painter;
|
||||
|
||||
QPainter& pen_only(const QPen& pen) {
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(pen);
|
||||
return painter;
|
||||
}
|
||||
QPainter& brush_only(const QBrush& brush) {
|
||||
painter.setBrush(brush);
|
||||
painter.setPen(Qt::NoPen);
|
||||
return painter;
|
||||
}
|
||||
|
||||
static auto make_rounded_rect_path(
|
||||
const QRectF& rect, qreal tl, qreal tr, qreal br, qreal bl) noexcept -> QPainterPath {
|
||||
|
||||
auto path = QPainterPath {};
|
||||
|
||||
const auto half_width = rect.width() / 2.0;
|
||||
const auto half_height = rect.height() / 2.0;
|
||||
|
||||
const auto max_radius = std::min(half_width, half_height);
|
||||
|
||||
const auto clamp_radius = [&](qreal r) {
|
||||
return r < 0 ? max_radius : std::min(r, max_radius);
|
||||
};
|
||||
tl = clamp_radius(tl);
|
||||
tr = clamp_radius(tr);
|
||||
br = clamp_radius(br);
|
||||
bl = clamp_radius(bl);
|
||||
|
||||
path.moveTo(rect.topLeft() + QPointF(tl, 0));
|
||||
|
||||
path.lineTo(rect.topRight() - QPointF(tr, 0));
|
||||
path.arcTo(
|
||||
QRectF(rect.topRight().x() - 2 * tr, rect.topRight().y(), 2 * tr, 2 * tr), 90, -90);
|
||||
|
||||
path.lineTo(rect.bottomRight() - QPointF(0, br));
|
||||
path.arcTo(QRectF(rect.bottomRight().x() - 2 * br, rect.bottomRight().y() - 2 * br, 2 * br,
|
||||
2 * br),
|
||||
0, -90);
|
||||
|
||||
path.lineTo(rect.bottomLeft() + QPointF(bl, 0));
|
||||
path.arcTo(QRectF(rect.bottomLeft().x(), rect.bottomLeft().y() - 2 * bl, 2 * bl, 2 * bl),
|
||||
270, -90);
|
||||
|
||||
path.lineTo(rect.topLeft() + QPointF(0, tl));
|
||||
path.arcTo(QRectF(rect.topLeft().x(), rect.topLeft().y(), 2 * tl, 2 * tl), 180, -90);
|
||||
|
||||
path.closeSubpath();
|
||||
|
||||
return path;
|
||||
}
|
||||
};
|
||||
}
|
||||
200
modern-qt/utility/painter/shape.hh
Normal file
200
modern-qt/utility/painter/shape.hh
Normal file
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/painter/common.hh"
|
||||
#include <qpainterpath.h>
|
||||
|
||||
namespace creeper::painter::internal {
|
||||
|
||||
struct EraseRectangle : public CommonProps {
|
||||
auto operator()(qt::painter& painter) const noexcept {
|
||||
painter.save();
|
||||
|
||||
painter.setCompositionMode(QPainter::CompositionMode_DestinationOut);
|
||||
|
||||
painter.setBrush(Qt::black);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRect(rect());
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
};
|
||||
|
||||
struct Rectangle : public CommonProps, ShapeProps {
|
||||
auto operator()(qt::painter& painter) const noexcept {
|
||||
painter.save();
|
||||
|
||||
const auto rectangle = qt::rect { origin, size };
|
||||
|
||||
painter.setBrush(container_color);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRect(rectangle);
|
||||
|
||||
if (outline_width > 0) {
|
||||
const auto thickness = outline_width / 2;
|
||||
const auto inliner = rectangle.adjusted(thickness, thickness, -thickness, -thickness);
|
||||
|
||||
painter.setPen({ outline_color, outline_width });
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(inliner);
|
||||
}
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
};
|
||||
|
||||
struct RoundedRectangle : public CommonProps, ShapeProps {
|
||||
double radius_tl = 0;
|
||||
double radius_tr = 0;
|
||||
double radius_bl = 0;
|
||||
double radius_br = 0;
|
||||
|
||||
auto set_radiuses(double r) {
|
||||
radius_tl = r;
|
||||
radius_tr = r;
|
||||
radius_bl = r;
|
||||
radius_br = r;
|
||||
}
|
||||
|
||||
auto operator()(qt::painter& painter) const noexcept {
|
||||
painter.save();
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const auto rect = qt::rect { origin, size };
|
||||
const auto outline_shape =
|
||||
make_rounded_rect_path(rect, radius_tl, radius_tr, radius_br, radius_bl);
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(container_color);
|
||||
painter.drawPath(outline_shape);
|
||||
|
||||
if (outline_width > 0) {
|
||||
const auto thickness = outline_width;
|
||||
const auto inliner_f = [=](double r) { return std::max(r - thickness / 2, 0.); };
|
||||
const auto inliner_rect =
|
||||
rect.adjusted(thickness / 2, thickness / 2, -thickness / 2, -thickness / 2);
|
||||
const auto inliner_shape = make_rounded_rect_path(inliner_rect, inliner_f(radius_tl),
|
||||
inliner_f(radius_tr), inliner_f(radius_br), inliner_f(radius_bl));
|
||||
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen({ outline_color, outline_width });
|
||||
painter.drawPath(inliner_shape);
|
||||
}
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
static constexpr auto make_rounded_rect_path(
|
||||
const qt::rect& rect, qreal tl, qreal tr, qreal br, qreal bl) noexcept -> QPainterPath {
|
||||
|
||||
auto path = QPainterPath {};
|
||||
|
||||
const auto max_radius = std::min(rect.width(), rect.height()) / 2.0;
|
||||
const auto clamp = [&](qreal r) -> qreal {
|
||||
return r < 0 ? max_radius : std::min(r, max_radius);
|
||||
};
|
||||
|
||||
tl = clamp(tl);
|
||||
tr = clamp(tr);
|
||||
br = clamp(br);
|
||||
bl = clamp(bl);
|
||||
|
||||
const auto Arc = [](qreal x, qreal y, qreal r,
|
||||
int start_angle) -> std::tuple<qt::rect, int, int> {
|
||||
return { qt::rect(x, y, 2 * r, 2 * r), start_angle, -90 };
|
||||
};
|
||||
|
||||
path.moveTo(rect.topLeft() + qt::point(tl, 0));
|
||||
path.lineTo(rect.topRight() - qt::point(tr, 0));
|
||||
const auto [tr_rect, tr_start, tr_span] =
|
||||
Arc(rect.topRight().x() - 2 * tr, rect.topRight().y(), tr, 90);
|
||||
path.arcTo(tr_rect, tr_start, tr_span);
|
||||
|
||||
path.lineTo(rect.bottomRight() - qt::point(0, br));
|
||||
const auto [br_rect, br_start, br_span] =
|
||||
Arc(rect.bottomRight().x() - 2 * br, rect.bottomRight().y() - 2 * br, br, 0);
|
||||
path.arcTo(br_rect, br_start, br_span);
|
||||
|
||||
path.lineTo(rect.bottomLeft() + qt::point(bl, 0));
|
||||
const auto [bl_rect, bl_start, bl_span] =
|
||||
Arc(rect.bottomLeft().x(), rect.bottomLeft().y() - 2 * bl, bl, 270);
|
||||
path.arcTo(bl_rect, bl_start, bl_span);
|
||||
|
||||
path.lineTo(rect.topLeft() + qt::point(0, tl));
|
||||
const auto [tl_rect, tl_start, tl_span] =
|
||||
Arc(rect.topLeft().x(), rect.topLeft().y(), tl, 180);
|
||||
path.arcTo(tl_rect, tl_start, tl_span);
|
||||
|
||||
path.closeSubpath();
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
struct Text : CommonProps {
|
||||
qt::string text;
|
||||
qt::font font;
|
||||
qt::color color = Qt::black;
|
||||
qt::text_option text_option;
|
||||
qt::real scale = 1.;
|
||||
|
||||
auto operator()(qt::painter& painter) const noexcept {
|
||||
painter.save();
|
||||
painter.scale(scale, scale);
|
||||
|
||||
const auto origin_rect = rect();
|
||||
const auto offset_x = origin_rect.x() * (1.0 - scale);
|
||||
const auto center_y = origin_rect.y() + origin_rect.height() / 2.0;
|
||||
const auto offset_y = center_y * (1.0 - scale);
|
||||
painter.translate(offset_x, offset_y);
|
||||
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(color);
|
||||
painter.setFont(font);
|
||||
|
||||
const auto scaled_rect = qt::rect {
|
||||
origin_rect.x() / scale,
|
||||
origin_rect.y() / scale,
|
||||
origin_rect.width() / scale,
|
||||
origin_rect.height() / scale,
|
||||
};
|
||||
painter.drawText(scaled_rect, text, text_option);
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::painter {
|
||||
|
||||
/// Export Rounded Rectangle
|
||||
|
||||
using RadiusTL = SetterProp<common::pro::Token, double,
|
||||
[](auto& self, auto radius) { self.radius_tl = radius; }>;
|
||||
using RadiusTR = SetterProp<common::pro::Token, double,
|
||||
[](auto& self, auto radius) { self.radius_tr = radius; }>;
|
||||
using RadiusBL = SetterProp<common::pro::Token, double,
|
||||
[](auto& self, auto radius) { self.radius_bl = radius; }>;
|
||||
using RadiusBR = SetterProp<common::pro::Token, double,
|
||||
[](auto& self, auto radius) { self.radius_br = radius; }>;
|
||||
using Radiuses = SetterProp<common::pro::Token, double,
|
||||
[](auto& self, auto radius) { self.set_radiuses(radius); }>;
|
||||
|
||||
/// Export Text
|
||||
using Text = DerivedProp<common::pro::Token, qt::string,
|
||||
[](auto& self, const auto& text) { self.text = text; }>;
|
||||
using Font = DerivedProp<common::pro::Token, qt::font,
|
||||
[](auto& self, const auto& font) { self.font = font; }>;
|
||||
using Color = DerivedProp<common::pro::Token, qt::color,
|
||||
[](auto& self, const auto& color) { self.color = color; }>;
|
||||
using Scale = SetterProp<common::pro::Token, qt::real,
|
||||
[](auto& self, const auto& scale) { self.scale = scale; }>;
|
||||
|
||||
using TextOption = DerivedProp<common::pro::Token, qt::text_option,
|
||||
[](auto& self, const auto& option) { self.text_option = option; }>;
|
||||
|
||||
namespace Paint {
|
||||
using EraseRectangle = Declarative<internal::EraseRectangle, CheckerOr<common::pro::checker>>;
|
||||
using Rectangle = Declarative<internal::Rectangle, CheckerOr<common::pro::checker>>;
|
||||
using RoundedRectangle =
|
||||
Declarative<internal::RoundedRectangle, CheckerOr<common::pro::checker>>;
|
||||
using Text = Declarative<internal::Text, CheckerOr<common::pro::checker>>;
|
||||
}
|
||||
|
||||
}
|
||||
19
modern-qt/utility/qt_wrapper/enter_event.hh
Normal file
19
modern-qt/utility/qt_wrapper/enter_event.hh
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <QEnterEvent>
|
||||
#else
|
||||
#include <QEvent>
|
||||
#endif
|
||||
|
||||
namespace creeper::qt {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
using EnterEvent = QEnterEvent;
|
||||
#else
|
||||
using EnterEvent = QEvent;
|
||||
#endif
|
||||
|
||||
}
|
||||
15
modern-qt/utility/qt_wrapper/margin_setter.hh
Normal file
15
modern-qt/utility/qt_wrapper/margin_setter.hh
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace creeper::qt {
|
||||
|
||||
inline auto margin_setter = [](auto& self, const auto& margin) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
self.setContentsMargins(margin, margin, margin, margin);
|
||||
#else
|
||||
self.setMargin(margin);
|
||||
#endif
|
||||
};
|
||||
|
||||
}
|
||||
54
modern-qt/utility/solution/round-angle.cc
Normal file
54
modern-qt/utility/solution/round-angle.cc
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "round-angle.hh"
|
||||
#include <eigen3/Eigen/Dense>
|
||||
|
||||
using namespace creeper;
|
||||
using Eigen::Vector2d, std::numbers::pi;
|
||||
|
||||
static inline auto impl_round_angle_solution(
|
||||
RoundAngleSolution& solution, Vector2d e0, Vector2d e1, Vector2d e2, double radius) -> void {
|
||||
|
||||
// solve the arc origin
|
||||
const auto v1 = Vector2d { e1 - e0 };
|
||||
const auto v2 = Vector2d { e2 - e0 };
|
||||
|
||||
const auto dot = v1.x() * v2.x() + v1.y() * v2.y();
|
||||
const auto det = v1.x() * v2.y() - v1.y() * v2.x();
|
||||
|
||||
const auto angle = std::abs(std::atan2(det, dot));
|
||||
const auto width = radius / std::tan(angle / 2);
|
||||
|
||||
const auto point_begin = Vector2d { e0 + width * v1.normalized() };
|
||||
const auto point_end = Vector2d { e0 + width * v2.normalized() };
|
||||
|
||||
const auto origin = Vector2d { point_begin + radius * v1.unitOrthogonal() };
|
||||
|
||||
solution.start = QPointF { point_begin.x(), point_begin.y() };
|
||||
solution.end = QPointF { point_end.x(), point_end.y() };
|
||||
|
||||
// solve the rect corners
|
||||
const auto v3 = Vector2d { e0 - origin }.normalized();
|
||||
const auto v4 = Vector2d { v3.unitOrthogonal() };
|
||||
const Vector2d corner0 = origin + Vector2d::UnitX() * radius + Vector2d::UnitY() * radius;
|
||||
const Vector2d corner1 = origin - Vector2d::UnitX() * radius - Vector2d::UnitY() * radius;
|
||||
solution.rect = QRectF { QPointF(corner1.x(), corner1.y()), QPointF(corner0.x(), corner0.y()) };
|
||||
|
||||
// solve the arc angle
|
||||
// 角度计算时,注意Qt的系Y的正方向向下,但角度又是从X正方向逆时针开始计算,可谓混乱
|
||||
const auto angle_begin_vector = Vector2d { point_begin - origin };
|
||||
const auto angle_end_vector = Vector2d { point_end - origin };
|
||||
|
||||
const auto angle_end = std::atan2(-angle_end_vector.y(), angle_end_vector.x());
|
||||
|
||||
solution.angle_begin = std::atan2(-angle_begin_vector.y(), angle_begin_vector.x());
|
||||
solution.angle_length = angle_end - solution.angle_begin;
|
||||
|
||||
if (solution.angle_length < -pi) solution.angle_length = 2 * pi + solution.angle_length;
|
||||
|
||||
solution.angle_begin = solution.angle_begin * 180 / pi;
|
||||
solution.angle_length = solution.angle_length * 180 / pi;
|
||||
}
|
||||
|
||||
RoundAngleSolution::RoundAngleSolution(QPointF e0, QPointF e1, QPointF e2, double radius) noexcept {
|
||||
impl_round_angle_solution(
|
||||
*this, Eigen::Vector2d { e0.x(), e0.y() }, { e1.x(), e1.y() }, { e2.x(), e2.y() }, radius);
|
||||
}
|
||||
25
modern-qt/utility/solution/round-angle.hh
Normal file
25
modern-qt/utility/solution/round-angle.hh
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <qpoint.h>
|
||||
#include <qrect.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
struct RoundAngleSolution {
|
||||
|
||||
/// @brief 给定原点和端点,按逆时针方向计算圆弧
|
||||
/// @note 圆弧注意按照逆时针算
|
||||
/// @param e0 两切线交点
|
||||
/// @param e1 圆弧起始点切线
|
||||
/// @param e2 圆弧终点切线
|
||||
/// @param radius 半径
|
||||
RoundAngleSolution(QPointF e0, QPointF e1, QPointF e2, double radius) noexcept;
|
||||
|
||||
QRectF rect;
|
||||
QPointF start;
|
||||
QPointF end;
|
||||
double angle_begin;
|
||||
double angle_length;
|
||||
};
|
||||
|
||||
}
|
||||
60
modern-qt/utility/theme/color-scheme.hh
Normal file
60
modern-qt/utility/theme/color-scheme.hh
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcolor.h>
|
||||
#include <qfont.h>
|
||||
|
||||
namespace creeper::theme {
|
||||
|
||||
enum class ColorMode { LIGHT, DARK };
|
||||
|
||||
struct ColorScheme {
|
||||
QColor primary;
|
||||
QColor on_primary;
|
||||
QColor primary_container;
|
||||
QColor on_primary_container;
|
||||
|
||||
QColor secondary;
|
||||
QColor on_secondary;
|
||||
QColor secondary_container;
|
||||
QColor on_secondary_container;
|
||||
|
||||
QColor tertiary;
|
||||
QColor on_tertiary;
|
||||
QColor tertiary_container;
|
||||
QColor on_tertiary_container;
|
||||
|
||||
QColor error;
|
||||
QColor on_error;
|
||||
QColor error_container;
|
||||
QColor on_error_container;
|
||||
|
||||
QColor background;
|
||||
QColor on_background;
|
||||
QColor surface;
|
||||
QColor on_surface;
|
||||
QColor surface_variant;
|
||||
QColor on_surface_variant;
|
||||
|
||||
QColor outline;
|
||||
QColor outline_variant;
|
||||
QColor shadow;
|
||||
QColor scrim;
|
||||
|
||||
QColor inverse_surface;
|
||||
QColor inverse_on_surface;
|
||||
QColor inverse_primary;
|
||||
|
||||
QColor surface_container_highest;
|
||||
QColor surface_container_high;
|
||||
QColor surface_container;
|
||||
QColor surface_container_low;
|
||||
QColor surface_container_lowest;
|
||||
};
|
||||
|
||||
struct Typography {
|
||||
QFont body;
|
||||
QFont title;
|
||||
QFont button;
|
||||
};
|
||||
|
||||
}
|
||||
101
modern-qt/utility/theme/preset/blue-miku.hh
Normal file
101
modern-qt/utility/theme/preset/blue-miku.hh
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
|
||||
namespace creeper {
|
||||
|
||||
constexpr auto kBlueMikuLightColorScheme = ColorScheme {
|
||||
// 蓝色初音亮色
|
||||
.primary = QColor(0, 89, 199),
|
||||
.on_primary = QColor(255, 255, 255),
|
||||
.primary_container = QColor(217, 226, 255),
|
||||
.on_primary_container = QColor(0, 26, 67),
|
||||
|
||||
.secondary = QColor(87, 94, 113),
|
||||
.on_secondary = QColor(255, 255, 255),
|
||||
.secondary_container = QColor(219, 226, 249),
|
||||
.on_secondary_container = QColor(20, 27, 44),
|
||||
|
||||
.tertiary = QColor(114, 85, 115),
|
||||
.on_tertiary = QColor(255, 255, 255),
|
||||
.tertiary_container = QColor(252, 215, 251),
|
||||
.on_tertiary_container = QColor(42, 19, 45),
|
||||
|
||||
.error = QColor(186, 26, 26),
|
||||
.on_error = QColor(255, 255, 255),
|
||||
.error_container = QColor(255, 218, 214),
|
||||
.on_error_container = QColor(65, 0, 2),
|
||||
|
||||
.background = QColor(254, 251, 255),
|
||||
.on_background = QColor(27, 27, 31),
|
||||
.surface = QColor(254, 251, 255),
|
||||
.on_surface = QColor(27, 27, 31),
|
||||
.surface_variant = QColor(225, 226, 236),
|
||||
.on_surface_variant = QColor(68, 70, 79),
|
||||
|
||||
.outline = QColor(117, 119, 128),
|
||||
.outline_variant = QColor(197, 198, 208),
|
||||
.shadow = QColor(0, 0, 0),
|
||||
.scrim = QColor(0, 0, 0),
|
||||
|
||||
.inverse_surface = QColor(48, 48, 52),
|
||||
.inverse_on_surface = QColor(242, 240, 244),
|
||||
.inverse_primary = QColor(175, 198, 255),
|
||||
|
||||
.surface_container_highest = QColor(224, 232, 248),
|
||||
.surface_container_high = QColor(226, 233, 249),
|
||||
.surface_container = QColor(234, 238, 251),
|
||||
.surface_container_low = QColor(242, 243, 252),
|
||||
.surface_container_lowest = QColor(254, 251, 255),
|
||||
};
|
||||
constexpr auto kBlueMikuDarkColorScheme = ColorScheme {
|
||||
// 蓝色初音暗色
|
||||
.primary = QColor(175, 198, 255),
|
||||
.on_primary = QColor(0, 45, 108),
|
||||
.primary_container = QColor(0, 67, 152),
|
||||
.on_primary_container = QColor(217, 226, 255),
|
||||
|
||||
.secondary = QColor(191, 198, 220),
|
||||
.on_secondary = QColor(41, 48, 66),
|
||||
.secondary_container = QColor(63, 71, 89),
|
||||
.on_secondary_container = QColor(219, 226, 249),
|
||||
|
||||
.tertiary = QColor(223, 187, 222),
|
||||
.on_tertiary = QColor(64, 39, 67),
|
||||
.tertiary_container = QColor(89, 62, 90),
|
||||
.on_tertiary_container = QColor(252, 215, 251),
|
||||
|
||||
.error = QColor(255, 180, 171),
|
||||
.on_error = QColor(105, 0, 5),
|
||||
.error_container = QColor(147, 0, 10),
|
||||
.on_error_container = QColor(255, 180, 171),
|
||||
|
||||
.background = QColor(27, 27, 31),
|
||||
.on_background = QColor(227, 226, 230),
|
||||
.surface = QColor(27, 27, 31),
|
||||
.on_surface = QColor(227, 226, 230),
|
||||
.surface_variant = QColor(68, 70, 79),
|
||||
.on_surface_variant = QColor(197, 198, 208),
|
||||
|
||||
.outline = QColor(143, 144, 153),
|
||||
.outline_variant = QColor(68, 70, 79),
|
||||
.shadow = QColor(0, 0, 0),
|
||||
.scrim = QColor(0, 0, 0),
|
||||
|
||||
.inverse_surface = QColor(227, 226, 230),
|
||||
.inverse_on_surface = QColor(48, 48, 52),
|
||||
.inverse_primary = QColor(0, 89, 199),
|
||||
|
||||
.surface_container_highest = QColor(44, 47, 57),
|
||||
.surface_container_high = QColor(43, 46, 56),
|
||||
.surface_container = QColor(39, 40, 49),
|
||||
.surface_container_low = QColor(34, 35, 42),
|
||||
.surface_container_lowest = QColor(27, 27, 31),
|
||||
};
|
||||
|
||||
constexpr auto kBlueMikuThemePack = ThemePack {
|
||||
.light = kBlueMikuLightColorScheme,
|
||||
.dark = kBlueMikuDarkColorScheme,
|
||||
};
|
||||
|
||||
}
|
||||
103
modern-qt/utility/theme/preset/gloden-harvest.hh
Normal file
103
modern-qt/utility/theme/preset/gloden-harvest.hh
Normal file
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
|
||||
namespace creeper {
|
||||
|
||||
// 丰收金色亮色主题 (Golden Harvest Light Scheme)
|
||||
constexpr auto kGoldenHarvestLightColorScheme = ColorScheme {
|
||||
.primary = QColor(124, 94, 0),
|
||||
.on_primary = QColor(255, 255, 255),
|
||||
.primary_container = QColor(255, 223, 128),
|
||||
.on_primary_container = QColor(38, 28, 0),
|
||||
|
||||
.secondary = QColor(98, 91, 66),
|
||||
.on_secondary = QColor(255, 255, 255),
|
||||
.secondary_container = QColor(232, 224, 198),
|
||||
.on_secondary_container = QColor(30, 26, 10),
|
||||
|
||||
.tertiary = QColor(0, 77, 78),
|
||||
.on_tertiary = QColor(255, 255, 255),
|
||||
.tertiary_container = QColor(160, 232, 212),
|
||||
.on_tertiary_container = QColor(0, 32, 33),
|
||||
|
||||
.error = QColor(186, 26, 26),
|
||||
.on_error = QColor(255, 255, 255),
|
||||
.error_container = QColor(255, 218, 214),
|
||||
.on_error_container = QColor(65, 0, 2),
|
||||
|
||||
.background = QColor(255, 248, 236),
|
||||
.on_background = QColor(30, 26, 10),
|
||||
.surface = QColor(255, 248, 236),
|
||||
.on_surface = QColor(30, 26, 10),
|
||||
.surface_variant = QColor(232, 224, 198),
|
||||
.on_surface_variant = QColor(74, 69, 50),
|
||||
|
||||
.outline = QColor(124, 117, 89),
|
||||
.outline_variant = QColor(202, 196, 168),
|
||||
.shadow = QColor(0, 0, 0),
|
||||
.scrim = QColor(0, 0, 0),
|
||||
|
||||
.inverse_surface = QColor(51, 47, 30),
|
||||
.inverse_on_surface = QColor(250, 245, 230),
|
||||
.inverse_primary = QColor(255, 223, 128),
|
||||
|
||||
.surface_container_highest = QColor(240, 235, 220),
|
||||
.surface_container_high = QColor(245, 240, 225),
|
||||
.surface_container = QColor(250, 245, 230),
|
||||
.surface_container_low = QColor(255, 250, 235),
|
||||
.surface_container_lowest = QColor(255, 253, 240),
|
||||
};
|
||||
|
||||
// 丰收金色暗色主题 (Golden Harvest Dark Scheme)
|
||||
constexpr auto kGoldenHarvestDarkColorScheme = ColorScheme {
|
||||
.primary = QColor(255, 223, 128),
|
||||
.on_primary = QColor(38, 28, 0),
|
||||
.primary_container = QColor(124, 94, 0),
|
||||
.on_primary_container = QColor(255, 255, 255),
|
||||
|
||||
.secondary = QColor(204, 196, 168),
|
||||
.on_secondary = QColor(51, 47, 30),
|
||||
.secondary_container = QColor(74, 69, 50),
|
||||
.on_secondary_container = QColor(232, 224, 198),
|
||||
|
||||
.tertiary = QColor(160, 232, 212),
|
||||
.on_tertiary = QColor(0, 32, 33),
|
||||
.tertiary_container = QColor(0, 77, 78),
|
||||
.on_tertiary_container = QColor(160, 232, 212),
|
||||
|
||||
.error = QColor(255, 180, 171),
|
||||
.on_error = QColor(105, 0, 5),
|
||||
.error_container = QColor(147, 0, 10),
|
||||
.on_error_container = QColor(255, 180, 171),
|
||||
|
||||
.background = QColor(30, 26, 10),
|
||||
.on_background = QColor(240, 235, 220),
|
||||
.surface = QColor(30, 26, 10),
|
||||
.on_surface = QColor(240, 235, 220),
|
||||
.surface_variant = QColor(74, 69, 50),
|
||||
.on_surface_variant = QColor(202, 196, 168),
|
||||
|
||||
.outline = QColor(146, 144, 137),
|
||||
.outline_variant = QColor(78, 75, 65),
|
||||
.shadow = QColor(0, 0, 0),
|
||||
.scrim = QColor(0, 0, 0),
|
||||
|
||||
.inverse_surface = QColor(240, 235, 220),
|
||||
.inverse_on_surface = QColor(51, 47, 30),
|
||||
.inverse_primary = QColor(124, 94, 0),
|
||||
|
||||
.surface_container_highest = QColor(50, 48, 41),
|
||||
.surface_container_high = QColor(45, 43, 37),
|
||||
.surface_container = QColor(40, 38, 32),
|
||||
.surface_container_low = QColor(35, 33, 27),
|
||||
.surface_container_lowest = QColor(30, 26, 10),
|
||||
};
|
||||
|
||||
// 丰收金色主题包
|
||||
constexpr auto kGoldenHarvestThemePack = ThemePack {
|
||||
.light = kGoldenHarvestLightColorScheme,
|
||||
.dark = kGoldenHarvestDarkColorScheme,
|
||||
};
|
||||
|
||||
}
|
||||
107
modern-qt/utility/theme/preset/green.hh
Normal file
107
modern-qt/utility/theme/preset/green.hh
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
|
||||
namespace creeper {
|
||||
|
||||
// 橄榄绿亮色主题 (Light Theme)
|
||||
constexpr auto kGreenLightColorScheme = ColorScheme {
|
||||
// Light Scheme (Primary: #386A20)
|
||||
.primary = QColor(56, 106, 32),
|
||||
.on_primary = QColor(255, 255, 255),
|
||||
.primary_container = QColor(184, 246, 150),
|
||||
.on_primary_container = QColor(1, 34, 0),
|
||||
|
||||
.secondary = QColor(86, 98, 75),
|
||||
.on_secondary = QColor(255, 255, 255),
|
||||
.secondary_container = QColor(217, 231, 202),
|
||||
.on_secondary_container = QColor(20, 31, 11),
|
||||
|
||||
.tertiary = QColor(56, 102, 99),
|
||||
.on_tertiary = QColor(255, 255, 255),
|
||||
.tertiary_container = QColor(187, 236, 231),
|
||||
.on_tertiary_container = QColor(0, 32, 31),
|
||||
|
||||
.error = QColor(186, 26, 26),
|
||||
.on_error = QColor(255, 255, 255),
|
||||
.error_container = QColor(255, 218, 214),
|
||||
.on_error_container = QColor(65, 0, 2),
|
||||
|
||||
.background = QColor(252, 253, 246),
|
||||
.on_background = QColor(26, 28, 24),
|
||||
.surface = QColor(252, 253, 246),
|
||||
.on_surface = QColor(26, 28, 24),
|
||||
.surface_variant = QColor(222, 229, 212),
|
||||
.on_surface_variant = QColor(67, 72, 62),
|
||||
|
||||
.outline = QColor(116, 121, 109),
|
||||
.outline_variant = QColor(195, 201, 188),
|
||||
.shadow = QColor(0, 0, 0),
|
||||
.scrim = QColor(0, 0, 0),
|
||||
|
||||
.inverse_surface = QColor(47, 49, 45),
|
||||
.inverse_on_surface = QColor(241, 241, 235),
|
||||
.inverse_primary = QColor(157, 218, 125),
|
||||
|
||||
// Surface steps for Light Theme
|
||||
.surface_container_highest = QColor(230, 230, 223), // Surface 4
|
||||
.surface_container_high = QColor(236, 236, 229), // Surface 3
|
||||
.surface_container = QColor(241, 241, 235), // Surface 2
|
||||
.surface_container_low = QColor(246, 247, 240), // Surface 1
|
||||
.surface_container_lowest = QColor(255, 255, 255), // Surface 0
|
||||
};
|
||||
|
||||
// 橄榄绿暗色主题 (Dark Theme)
|
||||
constexpr auto kGreenDarkColorScheme = ColorScheme {
|
||||
// Dark Scheme (Primary: #9DDA7D)
|
||||
.primary = QColor(157, 218, 125),
|
||||
.on_primary = QColor(15, 57, 0),
|
||||
.primary_container = QColor(33, 81, 6),
|
||||
.on_primary_container = QColor(184, 246, 150),
|
||||
|
||||
.secondary = QColor(189, 203, 176),
|
||||
.on_secondary = QColor(42, 52, 32),
|
||||
.secondary_container = QColor(64, 74, 54),
|
||||
.on_secondary_container = QColor(217, 231, 202),
|
||||
|
||||
.tertiary = QColor(160, 208, 204),
|
||||
.on_tertiary = QColor(1, 55, 53),
|
||||
.tertiary_container = QColor(31, 78, 76),
|
||||
.on_tertiary_container = QColor(187, 236, 231),
|
||||
|
||||
.error = QColor(255, 180, 171),
|
||||
.on_error = QColor(105, 0, 5),
|
||||
.error_container = QColor(147, 0, 10),
|
||||
.on_error_container = QColor(255, 218, 214),
|
||||
|
||||
.background = QColor(26, 28, 24),
|
||||
.on_background = QColor(227, 227, 220),
|
||||
.surface = QColor(26, 28, 24),
|
||||
.on_surface = QColor(227, 227, 220),
|
||||
.surface_variant = QColor(67, 72, 62),
|
||||
.on_surface_variant = QColor(195, 201, 188),
|
||||
|
||||
.outline = QColor(142, 146, 135),
|
||||
.outline_variant = QColor(67, 72, 62),
|
||||
.shadow = QColor(0, 0, 0),
|
||||
.scrim = QColor(0, 0, 0),
|
||||
|
||||
.inverse_surface = QColor(227, 227, 220),
|
||||
.inverse_on_surface = QColor(47, 49, 45),
|
||||
.inverse_primary = QColor(56, 106, 32),
|
||||
|
||||
// Surface steps for Dark Theme
|
||||
.surface_container_highest = QColor(60, 65, 60), // Surface 4
|
||||
.surface_container_high = QColor(49, 54, 49), // Surface 3
|
||||
.surface_container = QColor(38, 43, 37), // Surface 2
|
||||
.surface_container_low = QColor(34, 37, 33), // Surface 1
|
||||
.surface_container_lowest = QColor(15, 20, 12), // Surface 0
|
||||
};
|
||||
|
||||
// 橄榄绿主题包
|
||||
constexpr auto kGreenThemePack = ThemePack {
|
||||
.light = kGreenLightColorScheme,
|
||||
.dark = kGreenDarkColorScheme,
|
||||
};
|
||||
|
||||
}
|
||||
72
modern-qt/utility/theme/theme.cc
Normal file
72
modern-qt/utility/theme/theme.cc
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
|
||||
using namespace creeper::theme;
|
||||
using Handler = ThemeManager::Handler;
|
||||
|
||||
struct ThemeManager::Impl {
|
||||
using Key = const QObject*;
|
||||
|
||||
std::unordered_map<Key, Handler> handlers;
|
||||
std::vector<Handler> begin_callbacks;
|
||||
std::vector<Handler> final_callbacks;
|
||||
ThemePack theme_pack;
|
||||
ColorMode color_mode;
|
||||
|
||||
auto apply_theme(const ThemeManager& manager) const {
|
||||
for (auto const& callback : begin_callbacks)
|
||||
callback(manager);
|
||||
for (auto& [_, callback] : handlers)
|
||||
callback(manager);
|
||||
for (auto const& callback : final_callbacks)
|
||||
callback(manager);
|
||||
}
|
||||
|
||||
auto append_handler(Key key, const Handler& handler) {
|
||||
handlers[key] = handler;
|
||||
QObject::connect(key, &QObject::destroyed, [this, key] { remove_handler(key); });
|
||||
}
|
||||
|
||||
void remove_handler(Key key) { handlers.erase(key); }
|
||||
};
|
||||
|
||||
ThemeManager::ThemeManager()
|
||||
: pimpl(std::make_unique<Impl>()) { }
|
||||
|
||||
ThemeManager::ThemeManager(const ThemePack& pack, ColorMode mode)
|
||||
: pimpl(std::make_unique<Impl>()) {
|
||||
pimpl->theme_pack = pack;
|
||||
pimpl->color_mode = mode;
|
||||
}
|
||||
|
||||
ThemeManager::~ThemeManager() = default;
|
||||
|
||||
void ThemeManager::apply_theme() const { pimpl->apply_theme(*this); }
|
||||
|
||||
void ThemeManager::append_handler(const QObject* key, const Handler& handler) {
|
||||
pimpl->append_handler(key, handler);
|
||||
}
|
||||
|
||||
auto ThemeManager::append_begin_callback(const Handler& callback) noexcept -> void {
|
||||
pimpl->begin_callbacks.push_back(callback);
|
||||
}
|
||||
auto ThemeManager::append_final_callback(const Handler& callback) noexcept -> void {
|
||||
pimpl->final_callbacks.push_back(callback);
|
||||
}
|
||||
|
||||
void ThemeManager::remove_handler(const QObject* key) { pimpl->remove_handler(key); }
|
||||
|
||||
void ThemeManager::set_theme_pack(const ThemePack& pack) { pimpl->theme_pack = pack; }
|
||||
void ThemeManager::set_color_mode(const ColorMode& mode) { pimpl->color_mode = mode; }
|
||||
|
||||
void ThemeManager::toggle_color_mode() {
|
||||
pimpl->color_mode = (pimpl->color_mode == ColorMode::LIGHT) //
|
||||
? ColorMode::DARK
|
||||
: ColorMode::LIGHT;
|
||||
}
|
||||
|
||||
ThemePack ThemeManager::theme_pack() const { return pimpl->theme_pack; }
|
||||
ColorMode ThemeManager::color_mode() const { return pimpl->color_mode; }
|
||||
|
||||
ColorScheme ThemeManager::color_scheme() const {
|
||||
return pimpl->theme_pack.color_scheme(pimpl->color_mode);
|
||||
}
|
||||
126
modern-qt/utility/theme/theme.hh
Normal file
126
modern-qt/utility/theme/theme.hh
Normal file
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <qwidget.h>
|
||||
|
||||
// #include "utility/theme/color-scheme.hh"
|
||||
// #include "utility/wrapper/common.hh"
|
||||
// #include "utility/wrapper/pimpl.hh"
|
||||
// #include "utility/wrapper/property.hh"
|
||||
#include "modern-qt/utility/theme/color-scheme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include <memory>
|
||||
|
||||
namespace creeper::theme {
|
||||
|
||||
class ThemeManager;
|
||||
|
||||
template <class T>
|
||||
concept color_scheme_setter_trait = requires(T t) {
|
||||
{ t.set_color_scheme(ColorScheme {}) };
|
||||
};
|
||||
template <class T>
|
||||
concept theme_manager_loader_trait =
|
||||
requires(T t, ThemeManager& manager) { t.load_theme_manager(manager); };
|
||||
|
||||
struct ThemePack {
|
||||
ColorScheme light, dark;
|
||||
auto color_scheme(this auto&& self, ColorMode mode) noexcept {
|
||||
return (mode == ColorMode::LIGHT) ? self.light : self.dark;
|
||||
}
|
||||
};
|
||||
|
||||
class ThemeManager {
|
||||
// CREEPER_PIMPL_DEFINITION(ThemeManager)
|
||||
public:
|
||||
ThemeManager();
|
||||
~ThemeManager();
|
||||
ThemeManager(const ThemeManager&) = delete;
|
||||
ThemeManager& operator=(const ThemeManager&) = delete;
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pimpl;
|
||||
public:
|
||||
|
||||
explicit ThemeManager(const ThemePack& pack, ColorMode mode = ColorMode::LIGHT);
|
||||
|
||||
void apply_theme() const;
|
||||
|
||||
using Handler = std::function<void(const ThemeManager&)>;
|
||||
|
||||
/// Registers a theme change callback for the specified widget.
|
||||
///
|
||||
/// When ThemeManager::apply_theme() is called, the registered handler will be executed.
|
||||
///
|
||||
/// Args:
|
||||
/// key: Pointer to the widget. Serves as the key in the handler map.
|
||||
/// handler: The callback function to register.
|
||||
///
|
||||
/// Note:
|
||||
/// When the widget is destroyed, ThemeManager::remove_handler() will be called automatically
|
||||
/// to remove the associated handler.
|
||||
void append_handler(const QObject* key, const Handler& handler);
|
||||
|
||||
auto append_handler(color_scheme_setter_trait auto& widget) { append_handler(&widget, widget); }
|
||||
auto append_handler(const QObject* key, color_scheme_setter_trait auto& widget) {
|
||||
const auto handler = [&widget](const ThemeManager& manager) {
|
||||
const auto color_mode = manager.color_mode();
|
||||
const auto theme_pack = manager.theme_pack();
|
||||
widget.set_color_scheme(theme_pack.color_scheme(color_mode));
|
||||
};
|
||||
append_handler(key, std::move(handler));
|
||||
}
|
||||
|
||||
auto append_begin_callback(const Handler&) noexcept -> void;
|
||||
auto append_final_callback(const Handler&) noexcept -> void;
|
||||
|
||||
void remove_handler(const QObject* key);
|
||||
|
||||
void set_theme_pack(const ThemePack& pack);
|
||||
void set_color_mode(const ColorMode& mode);
|
||||
void toggle_color_mode();
|
||||
|
||||
ThemePack theme_pack() const;
|
||||
ColorMode color_mode() const;
|
||||
|
||||
ColorScheme color_scheme() const;
|
||||
};
|
||||
|
||||
}
|
||||
namespace creeper::theme::pro {
|
||||
|
||||
using Token = common::Token<ThemeManager>;
|
||||
|
||||
struct ColorScheme : public theme::ColorScheme, Token {
|
||||
using theme::ColorScheme::ColorScheme;
|
||||
explicit ColorScheme(const theme::ColorScheme& p)
|
||||
: theme::ColorScheme(p) { }
|
||||
auto apply(color_scheme_setter_trait auto& self) const noexcept -> void {
|
||||
self.set_color_scheme(*this);
|
||||
}
|
||||
};
|
||||
|
||||
struct ThemeManager : Token {
|
||||
theme::ThemeManager& manager;
|
||||
explicit ThemeManager(theme::ThemeManager& p)
|
||||
: manager(p) { }
|
||||
auto apply(theme_manager_loader_trait auto& self) const noexcept -> void {
|
||||
self.load_theme_manager(manager);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
|
||||
}
|
||||
namespace creeper {
|
||||
|
||||
using ColorMode = theme::ColorMode;
|
||||
using ColorScheme = theme::ColorScheme;
|
||||
using ThemePack = theme::ThemePack;
|
||||
using ThemeManager = theme::ThemeManager;
|
||||
|
||||
}
|
||||
40
modern-qt/utility/trait/widget.hh
Normal file
40
modern-qt/utility/trait/widget.hh
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <concepts>
|
||||
#include <qwidget.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <class T>
|
||||
concept widget_trait = std::convertible_to<T*, QWidget*>;
|
||||
|
||||
template <class T>
|
||||
concept widget_pointer_trait = std::convertible_to<T, QWidget*>;
|
||||
|
||||
template <class T>
|
||||
concept layout_trait = std::convertible_to<T*, QLayout*>;
|
||||
|
||||
template <class T>
|
||||
concept layout_pointer_trait = std::convertible_to<T, QLayout*>;
|
||||
|
||||
template <class T>
|
||||
concept item_trait = widget_trait<T> || layout_trait<T>;
|
||||
|
||||
template <class T>
|
||||
concept linear_trait = requires(T t) {
|
||||
{ t.addWidget(std::declval<QWidget*>(), int {}, Qt::AlignCenter) };
|
||||
{ t.addLayout(std::declval<QLayout*>(), int {}) };
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept area_trait = requires(T t) {
|
||||
{ t.setWidget(std::declval<QWidget*>()) };
|
||||
{ t.setLayout(std::declval<QLayout*>()) };
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept selectable_trait = requires(T t) {
|
||||
{ std::as_const(t).selected() } -> std::convertible_to<bool>;
|
||||
{ t.set_selected(bool {}) };
|
||||
};
|
||||
|
||||
}
|
||||
215
modern-qt/utility/wrapper/common.hh
Normal file
215
modern-qt/utility/wrapper/common.hh
Normal file
@@ -0,0 +1,215 @@
|
||||
#pragma once
|
||||
|
||||
#include "property.hh"
|
||||
|
||||
#include <qbitmap.h>
|
||||
#include <qvector.h>
|
||||
#include <qdebug.h>
|
||||
#include <qicon.h>
|
||||
#include <qwidget.h>
|
||||
|
||||
namespace creeper::common {
|
||||
template <typename Instance>
|
||||
struct Token {
|
||||
void apply(auto& self) const {
|
||||
const auto self_name = typeid(self).name();
|
||||
const auto prop_name = typeid(this).name();
|
||||
qDebug() << "Unimplemented" << prop_name << "is called by" << self_name;
|
||||
}
|
||||
};
|
||||
|
||||
namespace pro {
|
||||
|
||||
// 设置组建透明度
|
||||
template <class Token>
|
||||
using Opacity = SetterProp<Token, double, [](auto& self, double v) { self.set_opacity(v); }>;
|
||||
|
||||
// 设置圆角(NXNY)
|
||||
template <class Token>
|
||||
using RadiusNxNy =
|
||||
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_nx_ny(v); }>;
|
||||
|
||||
// 设置圆角(PXPY)
|
||||
template <class Token>
|
||||
using RadiusPxPy =
|
||||
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_px_py(v); }>;
|
||||
|
||||
// 设置圆角(NXPY)
|
||||
template <class Token>
|
||||
using RadiusNxPy =
|
||||
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_nx_py(v); }>;
|
||||
|
||||
// 设置圆角(PXNY)
|
||||
template <class Token>
|
||||
using RadiusPxNy =
|
||||
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_px_ny(v); }>;
|
||||
|
||||
// 设置圆角(X方向)
|
||||
template <class Token>
|
||||
using RadiusX = SetterProp<Token, double, [](auto& self, double v) { self.set_radius_x(v); }>;
|
||||
|
||||
// 设置圆角(Y方向)
|
||||
template <class Token>
|
||||
using RadiusY = SetterProp<Token, double, [](auto& self, double v) { self.set_radius_y(v); }>;
|
||||
|
||||
// 设置通用圆角
|
||||
template <class Token>
|
||||
using Radius = SetterProp<Token, double, [](auto& self, double v) { self.set_radius(v); }>;
|
||||
|
||||
// 通用边界宽度
|
||||
template <class Token>
|
||||
using BorderWidth =
|
||||
SetterProp<Token, double, [](auto& self, double v) { self.set_border_width(v); }>;
|
||||
|
||||
// 通用边界颜色
|
||||
template <class Token>
|
||||
using BorderColor =
|
||||
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_border_color(v); }>;
|
||||
|
||||
// 通用文字颜色
|
||||
template <class Token>
|
||||
using TextColor =
|
||||
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_text_color(v); }>;
|
||||
|
||||
// 通用背景颜色
|
||||
template <class Token>
|
||||
using Background =
|
||||
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_background(v); }>;
|
||||
|
||||
// 通用水波纹颜色
|
||||
template <class Token>
|
||||
using WaterColor =
|
||||
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_water_color(v); }>;
|
||||
|
||||
// 通用禁止属性
|
||||
template <class Token>
|
||||
using Disabled = SetterProp<Token, bool, [](auto& self, bool v) { self.set_disabled(v); }>;
|
||||
|
||||
// 通用 Checked 属性
|
||||
template <class Token>
|
||||
using Checked = SetterProp<Token, bool, [](auto& self, bool v) { self.set_checked(v); }>;
|
||||
|
||||
// 通用文本属性
|
||||
template <class Token, auto setter>
|
||||
struct String : public QString, Token {
|
||||
using QString::QString;
|
||||
|
||||
explicit String(const QString& text) noexcept
|
||||
: QString { text } { }
|
||||
explicit String(const std::string& text) noexcept
|
||||
: QString { QString::fromStdString(text) } { }
|
||||
|
||||
auto operator=(const QString& text) noexcept {
|
||||
QString::operator=(text);
|
||||
return *this;
|
||||
}
|
||||
auto operator=(QString&& text) noexcept {
|
||||
QString::operator=(std::move(text));
|
||||
return *this;
|
||||
}
|
||||
|
||||
void apply(auto& self) const
|
||||
requires requires { setter(self, *this); }
|
||||
{
|
||||
setter(self, *this);
|
||||
}
|
||||
};
|
||||
|
||||
template <class Token, typename T, auto setter>
|
||||
struct Vector : public QVector<T>, Token {
|
||||
using QVector<T>::QVector;
|
||||
|
||||
explicit Vector(const QVector<T>& vec) noexcept
|
||||
: QVector<T> { vec } { }
|
||||
explicit Vector(const std::vector<T>& vec) noexcept
|
||||
: QVector<T> { vec } { }
|
||||
|
||||
void apply(auto& self) const
|
||||
requires requires {setter(self, *this); }
|
||||
{
|
||||
setter(self, *this);
|
||||
}
|
||||
};
|
||||
|
||||
// template<class Token, typename T, auto setter>
|
||||
// Vector<Token, T, setter>::Vector(const QVector<T> &vec) noexcept:QVector { vec } { }
|
||||
|
||||
template <class Token>
|
||||
using Text = String<Token, [](auto& self, const auto& string) { self.setText(string); }>;
|
||||
|
||||
// 通用指针绑定
|
||||
template <class Token, class Final>
|
||||
struct Bind : Token {
|
||||
Final*& widget;
|
||||
explicit Bind(Final*& p)
|
||||
: widget(p) { };
|
||||
void apply(Final& self) const { widget = &self; }
|
||||
};
|
||||
|
||||
// 通用点击事件
|
||||
template <typename Callback, class Token>
|
||||
struct Clickable : Token {
|
||||
Callback callback;
|
||||
explicit Clickable(Callback callback) noexcept
|
||||
: callback { std::move(callback) } { }
|
||||
auto apply(auto& self) const noexcept -> void
|
||||
requires std::invocable<Callback, decltype(self)> || std::invocable<Callback>
|
||||
{
|
||||
using widget_t = std::remove_cvref_t<decltype(self)>;
|
||||
QObject::connect(&self, &widget_t::clicked, [function = callback, &self] {
|
||||
if constexpr (std::invocable<Callback, decltype(self)>) function(self);
|
||||
if constexpr (std::invocable<Callback>) function();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Callback, class Token>
|
||||
struct IndexChanged : Token {
|
||||
Callback callback;
|
||||
explicit IndexChanged(Callback callback) noexcept
|
||||
: callback {std::move(callback)} {}
|
||||
auto apply(auto& self) const noexcept -> void
|
||||
requires std::invocable<Callback, decltype(self)> || std::invocable<Callback> {
|
||||
using widget_t = std::remove_cvref_t<decltype(self)>;
|
||||
QObject::connect(&self, &widget_t::currentIndexChanged, [function = callback, &self] {
|
||||
if constexpr (std::invocable<Callback, decltype(self)>) function(self);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义信号回调注册
|
||||
|
||||
namespace internal {
|
||||
template <typename T>
|
||||
struct FunctionArgs;
|
||||
|
||||
template <class C, class R, class... Args>
|
||||
struct FunctionArgs<auto (C::*)(Args...)->R> {
|
||||
using type = std::tuple<Args...>;
|
||||
};
|
||||
template <class C, class R, class... Args>
|
||||
struct FunctionArgs<auto (C::*)(Args...) const->R> {
|
||||
using type = std::tuple<Args...>;
|
||||
};
|
||||
|
||||
template <typename F, typename Tuple>
|
||||
concept tuple_invocable_trait = requires(F&& f, Tuple&& t) {
|
||||
std::apply(std::forward<F>(f), std::forward<Tuple>(t)); //
|
||||
};
|
||||
}
|
||||
|
||||
template <typename F, class Token, auto signal>
|
||||
struct SignalInjection : Token {
|
||||
F f;
|
||||
|
||||
using SignalArgs = typename internal::FunctionArgs<decltype(signal)>::type;
|
||||
|
||||
explicit SignalInjection(F f) noexcept
|
||||
requires internal::tuple_invocable_trait<F, SignalArgs>
|
||||
: f { std::forward<decltype(f)>(f) } { }
|
||||
|
||||
auto apply(auto& self) const noexcept { QObject::connect(&self, signal, f); }
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
53
modern-qt/utility/wrapper/layout.hh
Normal file
53
modern-qt/utility/wrapper/layout.hh
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/qt_wrapper/margin_setter.hh"
|
||||
#include "modern-qt/utility/trait/widget.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
|
||||
namespace creeper::layout::pro {
|
||||
|
||||
struct Layout { };
|
||||
using Token = common::Token<Layout>;
|
||||
|
||||
using ContentsMargin = SetterProp<Token, QMargins,
|
||||
[](auto& self, const auto& margins) { self.setContentsMargins(margins); }>;
|
||||
|
||||
using Alignment = SetterProp<Token, Qt::Alignment,
|
||||
[](auto& self, const auto& alignment) { self.setAlignment(alignment); }>;
|
||||
|
||||
using Spacing =
|
||||
SetterProp<Token, int, [](auto& self, const auto& spacing) { self.setSpacing(spacing); }>;
|
||||
|
||||
using Margin = SetterProp<Token, int, qt::margin_setter>;
|
||||
|
||||
template <widget_trait T>
|
||||
struct Widget : Token {
|
||||
|
||||
T* item_pointer = nullptr;
|
||||
|
||||
explicit Widget(T* pointer) noexcept
|
||||
: item_pointer { pointer } { }
|
||||
|
||||
explicit Widget(auto&&... args) noexcept
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: item_pointer { new T { std::forward<decltype(args)>(args)... } } { }
|
||||
|
||||
auto apply(auto& layout) const { layout.addWidget(item_pointer); }
|
||||
};
|
||||
|
||||
// 传入一个方法用来辅助构造,在没有想要的接口时用这个吧
|
||||
template <typename Lambda>
|
||||
struct Apply : Token {
|
||||
Lambda lambda;
|
||||
explicit Apply(Lambda lambda) noexcept
|
||||
: lambda { lambda } { }
|
||||
auto apply(auto& self) const noexcept -> void {
|
||||
if constexpr (std::invocable<Lambda>) lambda();
|
||||
if constexpr (std::invocable<Lambda, decltype(self)>) lambda(self);
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
}
|
||||
153
modern-qt/utility/wrapper/mutable-value.hh
Normal file
153
modern-qt/utility/wrapper/mutable-value.hh
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include <qobject.h>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
template <typename T>
|
||||
struct MutableValue final {
|
||||
T value;
|
||||
|
||||
struct Functor {
|
||||
virtual ~Functor() noexcept = default;
|
||||
virtual void update(const T&) = 0;
|
||||
};
|
||||
std::unordered_map<QObject*, std::unique_ptr<Functor>> callbacks;
|
||||
|
||||
struct Nothing { };
|
||||
std::shared_ptr<Nothing> alive = std::make_shared<Nothing>();
|
||||
|
||||
MutableValue(const MutableValue&) = delete;
|
||||
MutableValue(MutableValue&&) = delete;
|
||||
|
||||
template <typename... Args>
|
||||
explicit MutableValue(Args&&... args) noexcept
|
||||
requires std::constructible_from<T, Args&&...>
|
||||
: value { std::forward<Args>(args)... } { }
|
||||
|
||||
constexpr auto get() const noexcept -> T { return value; }
|
||||
constexpr operator T() const noexcept { return get(); }
|
||||
|
||||
template <typename U>
|
||||
auto set(U&& u) noexcept -> void
|
||||
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
|
||||
{
|
||||
value = std::forward<U>(u);
|
||||
for (const auto& [_, f] : callbacks)
|
||||
f->update(value);
|
||||
}
|
||||
template <typename U>
|
||||
auto set_silent(U&& u) noexcept -> void
|
||||
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
|
||||
{
|
||||
value = std::forward<U>(u);
|
||||
}
|
||||
template <typename U>
|
||||
auto operator=(U&& u) noexcept -> void
|
||||
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
|
||||
{
|
||||
set(std::forward<U>(u));
|
||||
}
|
||||
};
|
||||
|
||||
template <class P, typename T>
|
||||
struct MutableForward final : public P {
|
||||
|
||||
MutableValue<T>& mutable_value;
|
||||
|
||||
explicit MutableForward(MutableValue<T>& m) noexcept
|
||||
requires std::constructible_from<P, T>
|
||||
: mutable_value { m }
|
||||
, P { m.get() } { }
|
||||
|
||||
template <template <typename> class Smart>
|
||||
explicit MutableForward(const Smart<MutableValue<T>>& m) noexcept
|
||||
requires std::constructible_from<P, T>
|
||||
: MutableForward { *m } { }
|
||||
|
||||
explicit MutableForward(const P&, MutableValue<T>& m) noexcept
|
||||
requires std::constructible_from<P, T>
|
||||
: MutableForward { m } { }
|
||||
|
||||
template <template <typename> class Smart>
|
||||
explicit MutableForward(const P&, const Smart<MutableValue<T>>& m) noexcept
|
||||
requires std::constructible_from<P, T>
|
||||
: MutableForward { m } { }
|
||||
|
||||
auto attach_callback_to_mutable(auto& widget) noexcept {
|
||||
struct Functor : MutableValue<T>::Functor {
|
||||
decltype(widget) w;
|
||||
Functor(decltype(w) w) noexcept
|
||||
: w { w } { }
|
||||
~Functor() noexcept = default;
|
||||
auto update(const T& value) -> void override {
|
||||
w.apply(P { value });
|
||||
w.update();
|
||||
}
|
||||
};
|
||||
auto functor = std::make_unique<Functor>(widget);
|
||||
|
||||
mutable_value.callbacks[&widget] = std::move(functor);
|
||||
|
||||
auto alive = std::weak_ptr { mutable_value.alive };
|
||||
QObject::connect(&widget, &QObject::destroyed, [this, alive](auto* key) {
|
||||
if (alive.lock()) mutable_value.callbacks.erase(key);
|
||||
});
|
||||
}
|
||||
auto apply(auto& widget) noexcept -> void {
|
||||
P::apply(widget);
|
||||
attach_callback_to_mutable(widget);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T, typename F>
|
||||
struct MutableTransform : widget::pro::Token {
|
||||
F apply_function;
|
||||
MutableValue<T>& mutable_value;
|
||||
|
||||
explicit MutableTransform(F f, MutableValue<T>& t) noexcept
|
||||
: mutable_value { t }
|
||||
, apply_function { std::move(f) } { }
|
||||
|
||||
template <template <typename> class Smart>
|
||||
explicit MutableTransform(F f, const Smart<MutableValue<T>>& m) noexcept
|
||||
: MutableTransform { std::move(f), *m } { }
|
||||
|
||||
auto attach_callback_to_mutable(auto& widget) noexcept {
|
||||
struct Functor : MutableValue<T>::Functor {
|
||||
decltype(widget) w;
|
||||
F apply_function;
|
||||
Functor(decltype(widget) w, F f) noexcept
|
||||
: w { w }
|
||||
, apply_function { std::move(f) } { }
|
||||
~Functor() noexcept = default;
|
||||
auto update(const T& value) -> void override {
|
||||
apply_function(w, value);
|
||||
w.update();
|
||||
}
|
||||
};
|
||||
auto functor = std::make_unique<Functor>(widget, std::move(apply_function));
|
||||
|
||||
mutable_value.callbacks[&widget] = std::move(functor);
|
||||
|
||||
auto alive = std::weak_ptr { mutable_value.alive };
|
||||
QObject::connect(&widget, &QObject::destroyed, [this, alive](auto* key) {
|
||||
if (alive.lock()) mutable_value.callbacks.erase(key);
|
||||
});
|
||||
}
|
||||
auto apply(auto& widget) noexcept -> void {
|
||||
apply_function(widget, mutable_value.get());
|
||||
attach_callback_to_mutable(widget);
|
||||
}
|
||||
};
|
||||
|
||||
using MutableQString = MutableValue<QString>;
|
||||
using MutableDouble = MutableValue<double>;
|
||||
using MutableFloat = MutableValue<float>;
|
||||
using MutableUInt8 = MutableValue<uint8_t>;
|
||||
using MutableUInt16 = MutableValue<uint16_t>;
|
||||
using MutableInt8 = MutableValue<int8_t>;
|
||||
using MutableInt16 = MutableValue<int16_t>;
|
||||
|
||||
}
|
||||
82
modern-qt/utility/wrapper/mutable.hh
Normal file
82
modern-qt/utility/wrapper/mutable.hh
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <qwidget.h>
|
||||
|
||||
namespace creeper {
|
||||
|
||||
/// @brief 响应式属性包装器,允许属性变更后自动同步到所有已绑定组件。
|
||||
///
|
||||
/// 该模板用于包装具备 apply 方法的属性类型,使其具备响应式能力。
|
||||
/// 通过 Mutable 包装后的属性可以被多个组件共享,
|
||||
/// 当属性发生变更时,所有通过 Mutable 绑定的组件会自动更新自身属性,
|
||||
/// 达到类似“全局属性联动”的效果。
|
||||
///
|
||||
/// Usage:
|
||||
/// auto mutable_size = Mutable{widget::pro::FixedSize{100, 100}};
|
||||
/// auto card_1 = OutlinedCard{mutable_size};
|
||||
/// auto card_2 = OutlinedCard{mutable_size};
|
||||
/// // 修改 mutable_size,card1/card2 会同步变化
|
||||
/// mutable_size = {mutable_size.width() + 10, mutable_size.height() + 10};
|
||||
///
|
||||
/// @note
|
||||
/// 1. 不支持将 Mutable 实例放入 std::tuple 进行批量 apply,
|
||||
/// 必须显式调用 apply 绑定组件。
|
||||
/// 若允许混入 tuple,容易导致属性的响应式副作用被隐式传播,
|
||||
/// 使得属性绑定和变更难以追踪,带来较大维护隐患。
|
||||
/// 显式 apply 可以让响应式属性的边界清晰可控,
|
||||
/// 便于代码审查和后期维护。
|
||||
///
|
||||
/// 2. Mutable 不支持拷贝和移动构造,需确保唯一性和生命周期安全。
|
||||
///
|
||||
/// 3. 绑定对象被销毁时,内部回调会自动解绑,无内存泄漏风险。
|
||||
///
|
||||
/// @date 2025.7.28
|
||||
template <class T>
|
||||
struct Mutable : public T {
|
||||
public:
|
||||
explicit Mutable(const T& t) noexcept
|
||||
: T { t } { }
|
||||
|
||||
explicit Mutable(std::constructible_from<T> auto&&... args) noexcept
|
||||
: T { std::forward<decltype(args)>(args)... } { }
|
||||
|
||||
Mutable(const Mutable&) = delete;
|
||||
Mutable(Mutable&&) = delete;
|
||||
|
||||
auto apply(auto& self) const noexcept -> void {
|
||||
T::apply(self);
|
||||
Mutable::bind(self);
|
||||
}
|
||||
auto bind(auto& self) const noexcept -> void {
|
||||
callbacks_.insert({
|
||||
&self,
|
||||
[&](const T& pro) {
|
||||
pro.apply(self);
|
||||
self.update();
|
||||
},
|
||||
});
|
||||
QObject::connect(&self, &QObject::destroyed, //
|
||||
[pointer = &self, this] { callbacks_.erase(pointer); });
|
||||
}
|
||||
template <typename U>
|
||||
auto set(U&& u) noexcept -> Mutable<T>&
|
||||
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
|
||||
{
|
||||
static_cast<T&>(*this) = std::forward<U>(u);
|
||||
for (const auto& [_, f] : callbacks_)
|
||||
f(*this);
|
||||
return *this;
|
||||
}
|
||||
template <typename U>
|
||||
auto operator=(U&& u) noexcept -> Mutable<T>&
|
||||
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
|
||||
{
|
||||
return set(std::forward<U>(u));
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::unordered_map<QWidget*, std::function<void(const T&)>> callbacks_;
|
||||
};
|
||||
|
||||
}
|
||||
18
modern-qt/utility/wrapper/pimpl.hh
Normal file
18
modern-qt/utility/wrapper/pimpl.hh
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#define CREEPER_PIMPL_DEFINITION(Class) \
|
||||
public: \
|
||||
Class(); \
|
||||
~Class(); \
|
||||
Class(const Class&) = delete; \
|
||||
Class& operator=(const Class&) = delete; \
|
||||
\
|
||||
private: \
|
||||
struct Impl; \
|
||||
std::unique_ptr<Impl> pimpl;
|
||||
|
||||
namespace creeper::internal {
|
||||
inline void use_std_unique_ptr() { auto unique_ptr = std::unique_ptr<int> {}; }
|
||||
}
|
||||
162
modern-qt/utility/wrapper/property.hh
Normal file
162
modern-qt/utility/wrapper/property.hh
Normal file
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
// 少量样板代码的生成宏是可以接受的,
|
||||
// 等 concept 可以作为参数的那一天,这个宏就可以废弃了
|
||||
#define CREEPER_DEFINE_CHECKER(TRAIT) \
|
||||
struct checker final { \
|
||||
template <class T> \
|
||||
static constexpr bool result = TRAIT<T>; \
|
||||
};
|
||||
|
||||
namespace creeper {
|
||||
|
||||
/// CHECKER
|
||||
template <class T>
|
||||
concept checker_trait = requires {
|
||||
{ T::template result<void> };
|
||||
};
|
||||
|
||||
template <checker_trait... Ts>
|
||||
struct CheckerOr {
|
||||
template <class T>
|
||||
static constexpr bool result = (Ts::template result<T> || ...);
|
||||
};
|
||||
template <checker_trait... Ts>
|
||||
struct CheckerAnd {
|
||||
template <class T>
|
||||
static constexpr bool result = (Ts::template result<T> && ...);
|
||||
};
|
||||
|
||||
/// PROP
|
||||
template <class Token, typename T, auto interface>
|
||||
struct SetterProp : Token {
|
||||
T value;
|
||||
|
||||
constexpr explicit SetterProp(T value) noexcept
|
||||
: value { std::move(value) } { }
|
||||
|
||||
constexpr explicit operator T(this auto&& self) noexcept { return self.value; }
|
||||
|
||||
template <typename O>
|
||||
auto operator=(O&& other) noexcept -> SetterProp&
|
||||
requires std::assignable_from<T&, O>
|
||||
{
|
||||
value = std::forward<O>(other);
|
||||
return *this; // x= y =6
|
||||
}
|
||||
|
||||
auto apply(auto& self) const noexcept -> void
|
||||
requires requires { interface(self, std::declval<T>()); }
|
||||
{
|
||||
interface(self, value);
|
||||
}
|
||||
};
|
||||
template <class Token, typename T, auto interface>
|
||||
struct DerivedProp : T, Token {
|
||||
using T::T;
|
||||
|
||||
template <typename... Args>
|
||||
requires std::constructible_from<T, Args&&...>
|
||||
constexpr explicit DerivedProp(Args&&... args)
|
||||
: T(std::forward<Args>(args)...) { }
|
||||
|
||||
template <typename U>
|
||||
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
|
||||
auto operator=(U&& value) -> DerivedProp& {
|
||||
T::operator=(std::forward<U>(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto apply(this auto const& self, auto& widget) noexcept -> void {
|
||||
interface(widget, static_cast<const T&>(self));
|
||||
}
|
||||
};
|
||||
template <class Token, auto interface>
|
||||
struct ActionProp : Token {
|
||||
auto apply(auto& self) const noexcept -> void
|
||||
requires requires { interface(self); }
|
||||
{
|
||||
interface(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief
|
||||
/// 声明式包装,非侵入式实现 Setter 的声明式化
|
||||
///
|
||||
/// 该包装器支持将 std::tuple 与单个 prop 混合传入,不受顺序限制。内部通过
|
||||
/// helper 检查 tuple 中的所有元素,递归调用 apply 将属性应用到 底层 Widget。
|
||||
/// 利用 CheckerT 延迟模板实例化,在 concept 层面约束属性类型, 从而避免因
|
||||
/// 递归参数展开而产生的海量且难以定位的模板错误。
|
||||
///
|
||||
/// @tparam W
|
||||
/// 需要被包装的组件类型。
|
||||
///
|
||||
/// @tparam checker
|
||||
/// 用于延迟模板实例化的“检查器”类型模板。典型形式如下:
|
||||
/// struct checker final {
|
||||
/// template <class T> struct result {
|
||||
/// static constexpr auto v = concept<T>;
|
||||
/// };
|
||||
/// };
|
||||
///
|
||||
/// @note
|
||||
/// 在不符合 checker 要求的类型被传入时,会在 concept 约束阶段直接报错,
|
||||
/// 提供简洁且精准的编译期错误提示,避免编译器自动展开大量构造函数
|
||||
/// 导致的冗长错误栈,但编译期报错信息依旧不友好。
|
||||
///
|
||||
template <class W, checker_trait checker>
|
||||
struct Declarative : public W {
|
||||
private:
|
||||
// For helpe check single props
|
||||
// 这里没法子塞一个 static_assert,无论如何这里都会被尝试错误地实例化
|
||||
template <typename T, class checker_>
|
||||
static constexpr auto impl_props_trait = checker_::template result<std::remove_cvref_t<T>>;
|
||||
|
||||
// For help check tuple of props
|
||||
// 使用 SFINAE 真是抱歉,没找到方便处理 tuple 的方法真是不好意思呢
|
||||
template <typename T, class checker_>
|
||||
static constexpr auto impl_tuple_trait = false;
|
||||
|
||||
template <typename... Ts, class checker_>
|
||||
static constexpr auto impl_tuple_trait<std::tuple<Ts...>, checker_> =
|
||||
(impl_props_trait<Ts, checker_> && ...);
|
||||
|
||||
public:
|
||||
/* Export widget type */
|
||||
using Widget = W;
|
||||
|
||||
/* Export checker type */
|
||||
using Checker = checker;
|
||||
|
||||
/* For check props */
|
||||
template <class T>
|
||||
static constexpr auto props_trait = impl_props_trait<std::remove_cvref_t<T>, checker>;
|
||||
|
||||
/* For check tuple */
|
||||
template <class T>
|
||||
static constexpr auto tuple_trait = impl_tuple_trait<std::remove_cvref_t<T>, checker>;
|
||||
|
||||
public:
|
||||
// 铁血的,热血的,冷血的声明式构造接口
|
||||
explicit Declarative(auto&&... props) noexcept
|
||||
requires((props_trait<decltype(props)> || tuple_trait<decltype(props)>) && ...)
|
||||
: W {} {
|
||||
(apply(std::forward<decltype(props)>(props)), ...);
|
||||
}
|
||||
auto apply(auto&& tuple) noexcept -> void
|
||||
requires tuple_trait<decltype(tuple)>
|
||||
{
|
||||
std::apply([this](auto&&... args) { (apply(std::forward<decltype(args)>(args)), ...); },
|
||||
std::forward<decltype(tuple)>(tuple));
|
||||
}
|
||||
auto apply(auto&& prop) noexcept -> void
|
||||
requires props_trait<decltype(prop)>
|
||||
{
|
||||
std::forward<decltype(prop)>(prop).apply(*this);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
23
modern-qt/utility/wrapper/singleton.hh
Normal file
23
modern-qt/utility/wrapper/singleton.hh
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
namespace creeper::util {
|
||||
|
||||
template <typename T> class Singleton {
|
||||
public:
|
||||
static T& instance();
|
||||
|
||||
Singleton(const Singleton&) = delete;
|
||||
Singleton& operator=(const Singleton&) = delete;
|
||||
|
||||
protected:
|
||||
struct token { };
|
||||
Singleton() = default;
|
||||
};
|
||||
|
||||
template <typename T> inline T& Singleton<T>::instance() {
|
||||
static const std::unique_ptr<T> instance { new T { token {} } };
|
||||
return *instance;
|
||||
}
|
||||
|
||||
}
|
||||
177
modern-qt/utility/wrapper/widget.hh
Normal file
177
modern-qt/utility/wrapper/widget.hh
Normal file
@@ -0,0 +1,177 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
|
||||
#include <qgraphicseffect.h>
|
||||
#include <qscreen.h>
|
||||
|
||||
namespace creeper::widget::pro {
|
||||
|
||||
using Token = common::Token<QWidget>;
|
||||
|
||||
using MinimumWidth = SetterProp<Token, int, [](auto& self, int v) { self.setMinimumWidth(v); }>;
|
||||
using MaximumWidth = SetterProp<Token, int, [](auto& self, int v) { self.setMaximumWidth(v); }>;
|
||||
using FixedWidth = SetterProp<Token, int, [](auto& self, int v) { self.setFixedWidth(v); }>;
|
||||
using MinimumHeight = SetterProp<Token, int, [](auto& self, int v) { self.setMinimumHeight(v); }>;
|
||||
using MaximumHeight = SetterProp<Token, int, [](auto& self, int v) { self.setMaximumHeight(v); }>;
|
||||
using FixedHeight = SetterProp<Token, int, [](auto& self, int v) { self.setFixedHeight(v); }>;
|
||||
|
||||
using LayoutDirection = SetterProp<Token, Qt::LayoutDirection,
|
||||
[](auto& self, Qt::LayoutDirection v) { self.setLayoutDirection(v); }>;
|
||||
using BackgroundRole = SetterProp<Token, QPalette::ColorRole,
|
||||
[](auto& self, QPalette::ColorRole v) { self.setBackgroundRole(v); }>;
|
||||
using ForegroundRole = SetterProp<Token, QPalette::ColorRole,
|
||||
[](auto& self, QPalette::ColorRole v) { self.setForegroundRole(v); }>;
|
||||
|
||||
using ClearMask = SetterProp<Token, std::nullptr_t, [](auto& self, auto) { self.clearMask(); }>;
|
||||
using GraphicsEffect = SetterProp<Token, QGraphicsEffect*,
|
||||
[](auto& self, QGraphicsEffect* v) { self.setGraphicsEffect(v); }>;
|
||||
|
||||
struct WindowFlag : Token {
|
||||
Qt::WindowType type;
|
||||
bool on;
|
||||
|
||||
explicit WindowFlag(Qt::WindowType type, bool on = true)
|
||||
: type { type }
|
||||
, on { on } { }
|
||||
|
||||
void apply(QWidget& widget) const { widget.setWindowFlag(type, on); }
|
||||
};
|
||||
using WindowFlags = SetterProp<Token, Qt::WindowFlags,
|
||||
[](auto& self, Qt::WindowFlags v) { self.setWindowFlags(v); }>;
|
||||
using WindowOpacity =
|
||||
SetterProp<Token, double, [](auto& self, double v) { self.setWindowOpacity(v); }>;
|
||||
|
||||
using Parent = SetterProp<Token, QWidget*, [](auto& self, QWidget* v) { self.setParent(v); }>;
|
||||
using Child = SetterProp<Token, QWidget*, [](auto& self, QWidget* v) { v->setParent(&self); }>;
|
||||
|
||||
using MinimumSize =
|
||||
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setMinimumSize(v); }>;
|
||||
using MaximumSize =
|
||||
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setMaximumSize(v); }>;
|
||||
using SizeIncrement =
|
||||
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setSizeIncrement(v); }>;
|
||||
using BaseSize = DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setBaseSize(v); }>;
|
||||
using FixedSize =
|
||||
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setFixedSize(v); }>;
|
||||
|
||||
using Font = DerivedProp<Token, QFont, [](auto& self, const QFont& v) { self.setFont(v); }>;
|
||||
using BitmapMask =
|
||||
DerivedProp<Token, QBitmap, [](auto& self, const QBitmap& v) { self.setMask(v); }>;
|
||||
using RegionMask =
|
||||
DerivedProp<Token, QRegion, [](auto& self, const QRegion& v) { self.setMask(v); }>;
|
||||
using WindowIcon =
|
||||
DerivedProp<Token, QIcon, [](auto& self, const QIcon& v) { self.setWindowIcon(v); }>;
|
||||
using WindowIconText =
|
||||
DerivedProp<Token, QString, [](auto& self, const QString& v) { self.setWindowIconText(v); }>;
|
||||
using WindowRole =
|
||||
DerivedProp<Token, QString, [](auto& self, const QString& v) { self.setWindowRole(v); }>;
|
||||
using WindowFilePath =
|
||||
DerivedProp<Token, QString, [](auto& self, const QString& v) { self.setWindowFilePath(v); }>;
|
||||
|
||||
using ToolTip =
|
||||
DerivedProp<Token, QString, [](auto& self, const QString& tip) { self.setToolTip(tip); }>;
|
||||
|
||||
struct MoveCenter : Token {
|
||||
auto apply(QWidget& self) const noexcept -> void {
|
||||
const auto screen = self.screen();
|
||||
|
||||
const auto screen_geometry = screen->availableGeometry();
|
||||
const auto screen_width = screen_geometry.width();
|
||||
const auto screen_height = screen_geometry.height();
|
||||
|
||||
const auto widget_geometry = self.geometry();
|
||||
const auto widget_width = widget_geometry.width();
|
||||
const auto widget_height = widget_geometry.height();
|
||||
|
||||
const auto x = (screen_width - widget_width) / 2;
|
||||
const auto y = (screen_height - widget_height) / 2;
|
||||
|
||||
self.move(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
struct SizePolicy : Token {
|
||||
QSizePolicy::Policy v, h;
|
||||
explicit SizePolicy(QSizePolicy::Policy policy)
|
||||
: v { policy }
|
||||
, h { policy } { }
|
||||
explicit SizePolicy(QSizePolicy::Policy v, QSizePolicy::Policy h)
|
||||
: v { v }
|
||||
, h { h } { }
|
||||
void apply(QWidget& self) const { self.setSizePolicy(h, v); }
|
||||
};
|
||||
|
||||
/// @note 该属性本质是转发构造,有 new 的行为
|
||||
template <class T>
|
||||
struct Layout : Token {
|
||||
T* layout_;
|
||||
|
||||
explicit Layout(T* pointer) noexcept
|
||||
requires std::convertible_to<decltype(pointer), QLayout*>
|
||||
: layout_ { pointer } { }
|
||||
|
||||
explicit Layout(auto&&... args)
|
||||
requires std::constructible_from<T, decltype(args)...>
|
||||
: layout_ { new T { std::forward<decltype(args)>(args)... } } { }
|
||||
|
||||
void apply(QWidget& widget) const { widget.setLayout(layout_); }
|
||||
};
|
||||
|
||||
/// @brief 绑定裸指针,为了避免赋值语句
|
||||
///
|
||||
/// 一般情况下需要绑定指针:
|
||||
///
|
||||
/// 假设下面是一个 layout 的内部
|
||||
/// clang-format 会在下面的赋值符号后换行
|
||||
/// @code
|
||||
/// auto widget = (Widget*)nullptr;
|
||||
/// ......
|
||||
/// { widget =
|
||||
/// new Widget {
|
||||
/// ......
|
||||
/// } },
|
||||
/// ......
|
||||
/// @endcode
|
||||
///
|
||||
/// 利用赋值语句的返回特性将该组件返回给 layout,同时完成赋值,
|
||||
/// 但这样会多一层缩进,为保证构造配置的简洁和同一,可以使用该包装:
|
||||
///
|
||||
/// @code
|
||||
/// ......
|
||||
/// { new Widget {
|
||||
/// pro::Bind { widget },
|
||||
/// } },
|
||||
/// ......
|
||||
/// @endcode
|
||||
///
|
||||
/// @tparam Final 需要绑定的组件类型(自动推导,无需显式指定)
|
||||
///
|
||||
/// @date 2025-06-19
|
||||
template <class Final>
|
||||
struct Bind : Token {
|
||||
Final*& widget;
|
||||
explicit Bind(Final*& widget) noexcept
|
||||
requires std::is_pointer<Final*>::value
|
||||
: widget(widget) { }
|
||||
void apply(Final& self) const noexcept { widget = &self; }
|
||||
};
|
||||
|
||||
// 传入一个方法用来辅助构造,在没有想要的接口时用这个吧
|
||||
template <typename Lambda>
|
||||
struct Apply : Token {
|
||||
Lambda lambda;
|
||||
explicit Apply(Lambda lambda) noexcept
|
||||
: lambda { lambda } { }
|
||||
auto apply(auto& self) const noexcept -> void {
|
||||
if constexpr (std::invocable<Lambda>) lambda();
|
||||
if constexpr (std::invocable<Lambda, decltype(self)>) lambda(self);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept trait = std::derived_from<T, Token>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
}
|
||||
21
modern-qt/widget.hh
Normal file
21
modern-qt/widget.hh
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "widget/buttons/button.hh"
|
||||
#include "widget/buttons/filled-button.hh"
|
||||
#include "widget/buttons/filled-tonal-button.hh"
|
||||
#include "widget/buttons/icon-button.hh"
|
||||
#include "widget/buttons/outlined-button.hh"
|
||||
#include "widget/buttons/text-button.hh"
|
||||
|
||||
#include "widget/cards/basic-card.hh"
|
||||
#include "widget/cards/elevated-card.hh"
|
||||
#include "widget/cards/filled-card.hh"
|
||||
#include "widget/cards/outlined-card.hh"
|
||||
|
||||
#include "widget/shape/ellipse.hh"
|
||||
#include "widget/shape/rounded-rect.hh"
|
||||
#include "widget/shape/shape.hh"
|
||||
|
||||
#include "widget/image.hh"
|
||||
#include "widget/main-window.hh"
|
||||
#include "widget/switch.hh"
|
||||
#include "widget/text-fields.hh"
|
||||
#include "widget/text.hh"
|
||||
26
modern-qt/widget/buttons/button.hh
Normal file
26
modern-qt/widget/buttons/button.hh
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-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)
|
||||
}
|
||||
170
modern-qt/widget/buttons/filled-button.cc
Normal file
170
modern-qt/widget/buttons/filled-button.cc
Normal file
@@ -0,0 +1,170 @@
|
||||
#include "filled-button.hh"
|
||||
|
||||
#include "modern-qt/utility/animation/math.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-qt/utility/animation/water-ripple.hh"
|
||||
#include "modern-qt/utility/painter/helper.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
|
||||
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 = 0;
|
||||
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
modern-qt/widget/buttons/filled-button.hh
Normal file
54
modern-qt/widget/buttons/filled-button.hh
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "modern-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
modern-qt/widget/buttons/filled-tonal-button.hh
Normal file
41
modern-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
modern-qt/widget/buttons/icon-button.cc
Normal file
44
modern-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
modern-qt/widget/buttons/icon-button.hh
Normal file
133
modern-qt/widget/buttons/icon-button.hh
Normal file
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
#include <qabstractbutton.h>
|
||||
#include <qpainter.h>
|
||||
|
||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-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>>;
|
||||
|
||||
}
|
||||
286
modern-qt/widget/buttons/icon-button.impl.hh
Normal file
286
modern-qt/widget/buttons/icon-button.impl.hh
Normal file
@@ -0,0 +1,286 @@
|
||||
#pragma once
|
||||
|
||||
#include "icon-button.hh"
|
||||
|
||||
#include "modern-qt/utility/animation/animatable.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/state/spring.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-qt/utility/animation/water-ripple.hh"
|
||||
#include "modern-qt/utility/material-icon.hh"
|
||||
#include "modern-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
modern-qt/widget/buttons/outlined-button.hh
Normal file
43
modern-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;
|
||||
}
|
||||
|
||||
using OutlinedButton =
|
||||
Declarative<outlined_button::internal::OutlinedButton, FilledButton::Checker>;
|
||||
}
|
||||
41
modern-qt/widget/buttons/text-button.hh
Normal file
41
modern-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
modern-qt/widget/cards/basic-card.hh
Normal file
97
modern-qt/widget/cards/basic-card.hh
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-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
modern-qt/widget/cards/elevated-card.hh
Normal file
39
modern-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
modern-qt/widget/cards/filled-card.hh
Normal file
9
modern-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
modern-qt/widget/cards/outlined-card.hh
Normal file
29
modern-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>;
|
||||
}
|
||||
32
modern-qt/widget/image.cc
Normal file
32
modern-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
modern-qt/widget/image.hh
Normal file
82
modern-qt/widget/image.hh
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/content-scale.hh"
|
||||
#include "modern-qt/utility/painter-resource.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-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
modern-qt/widget/image.impl.hh
Normal file
83
modern-qt/widget/image.impl.hh
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "modern-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
modern-qt/widget/indicator/circular-progress-indicator.hh
Normal file
25
modern-qt/widget/indicator/circular-progress-indicator.hh
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-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
modern-qt/widget/main-window.cc
Normal file
12
modern-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
modern-qt/widget/main-window.hh
Normal file
79
modern-qt/widget/main-window.hh
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
#include <qmainwindow.h>
|
||||
|
||||
#include "modern-qt/utility/trait/widget.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-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);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
94
modern-qt/widget/select.cc
Normal file
94
modern-qt/widget/select.cc
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
#include "select.hh"
|
||||
|
||||
#include "select.impl.hh"
|
||||
|
||||
BasicSelect::BasicSelect()
|
||||
: pimpl {std::make_unique<Impl>(*this)} {}
|
||||
|
||||
BasicSelect::~BasicSelect() = default;
|
||||
|
||||
void BasicSelect::set_color_scheme(const ColorScheme &scheme) {
|
||||
pimpl->set_color_scheme(scheme);
|
||||
}
|
||||
|
||||
void BasicSelect::load_theme_manager(ThemeManager &manager) {
|
||||
pimpl->load_theme_manager(manager);
|
||||
}
|
||||
|
||||
void BasicSelect::set_label_text(const QString &text) {
|
||||
pimpl->set_label_text(text);
|
||||
}
|
||||
|
||||
void BasicSelect::set_leading_icon(const QIcon &) {
|
||||
}
|
||||
|
||||
void BasicSelect::set_leading_icon(const QString &code, const QString &font) {
|
||||
pimpl->set_leading_icon(code, font);
|
||||
}
|
||||
|
||||
void BasicSelect::resizeEvent(QResizeEvent *event) {
|
||||
QComboBox::resizeEvent(event);
|
||||
}
|
||||
|
||||
void BasicSelect::enterEvent(qt::EnterEvent *enter_event) {
|
||||
pimpl->enter_event(enter_event);
|
||||
QComboBox::enterEvent(enter_event);
|
||||
}
|
||||
|
||||
void BasicSelect::leaveEvent(QEvent *event) {
|
||||
pimpl->leave_event(event);
|
||||
QComboBox::leaveEvent(event);
|
||||
}
|
||||
|
||||
void BasicSelect::focusInEvent(QFocusEvent *focus_event) {
|
||||
pimpl->focus_in(focus_event);
|
||||
QComboBox::focusInEvent(focus_event);
|
||||
}
|
||||
|
||||
void BasicSelect::focusOutEvent(QFocusEvent *event) {
|
||||
pimpl->focus_out(event);
|
||||
QComboBox::focusOutEvent(event);
|
||||
}
|
||||
|
||||
void BasicSelect::changeEvent(QEvent *event) {
|
||||
QComboBox::changeEvent(event);
|
||||
}
|
||||
|
||||
void BasicSelect::showPopup() {
|
||||
pimpl->show_popup();
|
||||
}
|
||||
|
||||
void BasicSelect::hidePopup() {
|
||||
pimpl->hide_popup();
|
||||
}
|
||||
|
||||
auto BasicSelect::set_measurements(const Measurements &measurements) noexcept -> void {
|
||||
pimpl->set_measurements(measurements);
|
||||
}
|
||||
|
||||
void BasicSelect::setTextMargins(int left, int top, int right, int bottom) {
|
||||
margins.setLeft(left);
|
||||
margins.setTop(top);
|
||||
margins.setRight(right);
|
||||
margins.setBottom(bottom);
|
||||
}
|
||||
|
||||
void BasicSelect::setTextMargins(const QMargins &margins) {
|
||||
this->margins = margins;
|
||||
}
|
||||
|
||||
QMargins BasicSelect::textMargins() const {
|
||||
return margins;
|
||||
}
|
||||
|
||||
|
||||
using namespace creeper;
|
||||
|
||||
void MatSelect::paintEvent(QPaintEvent *event) {
|
||||
pimpl->paint_filled(event);
|
||||
// QComboBox::paintEvent(event);
|
||||
}
|
||||
161
modern-qt/widget/select.hh
Normal file
161
modern-qt/widget/select.hh
Normal file
@@ -0,0 +1,161 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_COMBO_BOX_HH
|
||||
#define TOUCHSENSOR_COMBO_BOX_HH
|
||||
|
||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "modern-qt/utility/wrapper/mutable-value.hh"
|
||||
|
||||
#include <qcombobox.h>
|
||||
|
||||
namespace creeper {
|
||||
class MatSelect;
|
||||
|
||||
namespace select_widget::internal {
|
||||
class BasicSelect : public QComboBox {
|
||||
CREEPER_PIMPL_DEFINITION(BasicSelect);
|
||||
|
||||
friend MatSelect;
|
||||
|
||||
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 = 16;
|
||||
|
||||
int standard_font_height = 18;
|
||||
|
||||
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_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*) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
|
||||
void focusInEvent(QFocusEvent*) override;
|
||||
void focusOutEvent(QFocusEvent *event) override;
|
||||
|
||||
void changeEvent(QEvent *) override;
|
||||
|
||||
void showPopup() override;
|
||||
void hidePopup() override;
|
||||
|
||||
private:
|
||||
friend struct Impl;
|
||||
|
||||
public:
|
||||
void setTextMargins(int left, int top, int right, int bottom);
|
||||
void setTextMargins(const QMargins& margins);
|
||||
QMargins textMargins() const;
|
||||
|
||||
|
||||
private:
|
||||
QMargins margins{13, 24, 13, 0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace select_widget::pro {
|
||||
using Token = common::Token<internal::BasicSelect>;
|
||||
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);}
|
||||
};
|
||||
|
||||
struct LeadingText : Token {
|
||||
QString text;
|
||||
explicit LeadingText(const QString& text) : text {text} {}
|
||||
void apply(auto& self) const {self.set_label_text(text);};
|
||||
};
|
||||
|
||||
template<class Select>
|
||||
concept trait = std::derived_from<Select, Token>;
|
||||
|
||||
template<class Callback>
|
||||
using IndexChanged = common::pro::IndexChanged<Callback, Token>;
|
||||
|
||||
using SelectItems = common::pro::Vector<Token, QString,
|
||||
[](auto& self, const auto& vec) {self.clear();
|
||||
self.addItems(vec);
|
||||
self.setCurrentIndex(-1);
|
||||
}>;
|
||||
|
||||
CREEPER_DEFINE_CHECKER(trait);
|
||||
using namespace widget::pro;
|
||||
using namespace theme::pro;
|
||||
}
|
||||
struct MatSelect
|
||||
: public Declarative<select_widget::internal::BasicSelect,
|
||||
CheckerOr<select_widget::pro::checker, widget::pro::checker, theme::pro::checker>> {
|
||||
using Declarative::Declarative;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
};
|
||||
}
|
||||
#endif //TOUCHSENSOR_COMBO_BOX_HH
|
||||
387
modern-qt/widget/select.impl.hh
Normal file
387
modern-qt/widget/select.impl.hh
Normal file
@@ -0,0 +1,387 @@
|
||||
//
|
||||
// Created by Lenn on 2025/10/14.
|
||||
//
|
||||
|
||||
#ifndef TOUCHSENSOR_SELECT_IMPL_HH
|
||||
#define TOUCHSENSOR_SELECT_IMPL_HH
|
||||
|
||||
#include <qabstractitemview.h>
|
||||
|
||||
#include "select.hh"
|
||||
#include "modern-qt/utility/animation/animatable.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-qt/utility/painter/common.hh"
|
||||
#include "modern-qt/utility/painter/container.hh"
|
||||
#include "modern-qt/utility/painter/helper.hh"
|
||||
#include "modern-qt/utility/painter/shape.hh"
|
||||
|
||||
#include "qpainter.h"
|
||||
#include "qpainterpath.h"
|
||||
#include "select.hh"
|
||||
|
||||
using namespace creeper::select_widget::internal;
|
||||
|
||||
struct BasicSelect::Impl {
|
||||
public:
|
||||
explicit Impl(BasicSelect& self) noexcept
|
||||
: self{self}, animatable{self} {
|
||||
{
|
||||
auto state = std::make_shared<PidState<double>>();
|
||||
|
||||
state->config.kp = 20.0;
|
||||
state->config.ki = 00.0;
|
||||
state->config.kd = 00.0;
|
||||
state->config.epsilon = 1e-2;
|
||||
|
||||
label_position = make_transition(animatable, std::move(state));
|
||||
}
|
||||
|
||||
set_measurements(Measurements{});
|
||||
}
|
||||
|
||||
auto set_color_scheme(const ColorScheme& scheme) -> void {
|
||||
color_specs.enabled.container = scheme.surface_container_highest;
|
||||
color_specs.enabled.label_text = scheme.on_surface_variant;
|
||||
color_specs.enabled.selected_text = scheme.on_surface_variant;
|
||||
color_specs.enabled.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.enabled.active_indicator = scheme.on_surface_variant;
|
||||
color_specs.enabled.supporting_text = scheme.on_surface_variant;
|
||||
color_specs.enabled.input_text = scheme.on_surface;
|
||||
color_specs.enabled.caret = scheme.primary;
|
||||
color_specs.enabled.outline = scheme.outline;
|
||||
|
||||
color_specs.disabled.container = scheme.on_surface;
|
||||
color_specs.disabled.container.setAlphaF(0.04);
|
||||
color_specs.disabled.label_text = scheme.on_surface;
|
||||
color_specs.disabled.label_text.setAlphaF(0.38);
|
||||
color_specs.disabled.selected_text = scheme.on_surface;
|
||||
color_specs.disabled.selected_text.setAlphaF(0.04);
|
||||
color_specs.disabled.leading_icon = scheme.on_surface;
|
||||
color_specs.disabled.leading_icon.setAlphaF(0.38);
|
||||
color_specs.disabled.supporting_text = scheme.on_surface;
|
||||
color_specs.disabled.supporting_text.setAlphaF(0.38);
|
||||
color_specs.disabled.input_text = scheme.on_surface;
|
||||
color_specs.disabled.input_text.setAlphaF(0.38);
|
||||
color_specs.disabled.active_indicator = scheme.on_surface;
|
||||
color_specs.disabled.active_indicator.setAlphaF(0.38);
|
||||
color_specs.disabled.outline = scheme.outline;
|
||||
color_specs.disabled.outline.setAlphaF(0.38);
|
||||
|
||||
color_specs.focused.container = scheme.surface_container_highest;
|
||||
color_specs.focused.label_text = scheme.primary;
|
||||
color_specs.focused.selected_text = scheme.on_surface_variant;
|
||||
color_specs.focused.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.focused.input_text = scheme.on_surface;
|
||||
color_specs.focused.supporting_text = scheme.on_surface_variant;
|
||||
color_specs.focused.active_indicator = scheme.primary;
|
||||
color_specs.focused.outline = scheme.primary;
|
||||
|
||||
color_specs.error.container = scheme.surface_container_highest;
|
||||
color_specs.error.active_indicator = scheme.error;
|
||||
color_specs.error.label_text = scheme.error;
|
||||
color_specs.error.selected_text = scheme.on_surface;
|
||||
color_specs.error.input_text = scheme.on_surface;
|
||||
color_specs.error.supporting_text = scheme.error;
|
||||
color_specs.error.leading_icon = scheme.on_surface_variant;
|
||||
color_specs.error.caret = scheme.error;
|
||||
color_specs.error.outline = scheme.error;
|
||||
|
||||
color_specs.state_layer = scheme.on_surface;
|
||||
color_specs.state_layer.setAlphaF(0.08);
|
||||
color_specs.selection_container = scheme.primary;
|
||||
color_specs.selection_container.setAlphaF(0.38);
|
||||
|
||||
const auto& color = get_color_tokens();
|
||||
sync_basic_text_style(
|
||||
color.input_text,
|
||||
scheme.surface_container_highest,
|
||||
color.input_text,
|
||||
color_specs.selection_container,
|
||||
color_specs.selection_container
|
||||
);
|
||||
}
|
||||
|
||||
auto load_theme_manager(ThemeManager& manager) -> void {
|
||||
manager.append_handler(&self, [this](const ThemeManager& manager) {
|
||||
set_color_scheme(manager.color_scheme());
|
||||
});
|
||||
}
|
||||
|
||||
auto set_label_text(const QString& text) -> void { label_text = text; }
|
||||
|
||||
auto set_leading_icon(const QString& code, const QString& font) -> void {
|
||||
leading_icon_code = code;
|
||||
leading_icon_font = font;
|
||||
is_update_component_status = false;
|
||||
}
|
||||
|
||||
auto set_measurements(const Measurements& measurements) -> void {
|
||||
this->measurements = measurements;
|
||||
self.setFixedHeight(measurements.container_height + measurements.standard_font_height);
|
||||
is_update_component_status = false;
|
||||
}
|
||||
|
||||
auto paint_filled(QPaintEvent*) -> void {
|
||||
const auto rect = self.rect();
|
||||
const auto color = get_color_tokens();
|
||||
|
||||
|
||||
constexpr auto container_radius = 5;
|
||||
update_component_status();
|
||||
|
||||
auto painter = QPainter{&self};
|
||||
|
||||
{
|
||||
util::PainterHelper{painter}
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color.container, Qt::transparent, 0,
|
||||
rect, container_radius, container_radius, 0, 0);
|
||||
}
|
||||
|
||||
{
|
||||
const auto p0 = rect.bottomLeft();
|
||||
const auto p1 = rect.bottomRight();
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen{color.active_indicator, filled_line_width()});
|
||||
painter.drawLine(p0, p1);
|
||||
}
|
||||
|
||||
if (!leading_icon.isNull()) {
|
||||
// TODO: 要不要画个默认的
|
||||
}
|
||||
else if (!leading_icon_code.isEmpty()){
|
||||
const auto rect = QRectF {
|
||||
1.0 * (self.width() - self.textMargins().right() - measurements.icon_rect_size),
|
||||
0.5 * (self.height() - measurements.icon_rect_size),
|
||||
1.0 * measurements.icon_rect_size,
|
||||
1.0 * measurements.icon_rect_size,
|
||||
};
|
||||
|
||||
painter.save();
|
||||
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen {color.leading_icon});
|
||||
painter.setFont(leading_icon_font);
|
||||
|
||||
const auto icon_center = rect.center();
|
||||
painter.translate(icon_center);
|
||||
const bool is_active = (self.view() && self.view()->isVisible());
|
||||
painter.rotate(is_active ? 180.0 : 0.0);
|
||||
painter.translate(-icon_center);
|
||||
painter.drawText(rect, leading_icon_code, {Qt::AlignCenter});
|
||||
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
if (!label_text.isEmpty()) {
|
||||
const auto margins = self.textMargins();
|
||||
|
||||
const auto center_label_padding = 0.5 * (measurements.container_height);
|
||||
const auto rect_center = QRectF {
|
||||
QPointF {static_cast<double>(margins.left()), center_label_padding},
|
||||
QPointF (self.width() - margins.right(), self.height() - center_label_padding),
|
||||
};
|
||||
const auto rect_top = QRectF {
|
||||
QPointF (margins.left(), measurements.col_padding),
|
||||
QPointF (self.width() - margins.right(), margins.top()),
|
||||
};
|
||||
|
||||
const auto position = self.currentText().isEmpty() ? *label_position : 1.;
|
||||
const auto label_rect = animate::interpolate(rect_center, rect_top, position);
|
||||
const auto scale = 1. - position * 0.25;
|
||||
|
||||
painter.save();
|
||||
const auto label_anchor = QPointF {label_rect.left(), label_rect.center().y()};
|
||||
painter.translate(label_anchor);
|
||||
painter.scale(scale, scale);
|
||||
painter.translate(-label_anchor);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen {color.label_text});
|
||||
painter.setFont(standard_text_font);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.drawText(label_rect, label_text, {Qt::AlignVCenter | Qt::AlignLeading});
|
||||
painter.restore();
|
||||
|
||||
|
||||
if (self.currentIndex() != -1) {
|
||||
qDebug() << "paint currentText index: " << self.currentIndex();
|
||||
painter.save();
|
||||
const auto rect_center_selected = QRectF {
|
||||
QPointF {static_cast<double>(margins.left()), center_label_padding},
|
||||
QPointF (self.width() - margins.right(), self.height() - 0.5 * center_label_padding),
|
||||
};
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen {color.selected_text});
|
||||
painter.setFont(standard_text_font);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.drawText(rect_center_selected, self.currentText(), Qt::AlignVCenter | Qt::AlignLeading);
|
||||
painter.restore();
|
||||
}
|
||||
}
|
||||
else if (!label_text.isEmpty() && self.currentIndex() != -1) {
|
||||
const auto selected_text = self.currentText();
|
||||
const auto position = self.currentText().isEmpty() ? *label_position : 1.;
|
||||
}
|
||||
|
||||
if (is_hovered) {
|
||||
util::PainterHelper{painter}
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color_specs.state_layer, Qt::transparent, 0, rect,
|
||||
container_radius, container_radius, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
auto enter_event(qt::EnterEvent*) -> void {
|
||||
is_hovered = true;
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto leave_event(QEvent*) -> void {
|
||||
is_hovered = false;
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto focus_in(QFocusEvent*) -> void {
|
||||
is_focused = true;
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto focus_out(QFocusEvent*) -> void {
|
||||
is_focused = false;
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto show_popup() -> void {
|
||||
if (self.count() == 0) {
|
||||
return;
|
||||
}
|
||||
is_active = true;
|
||||
self.QComboBox::showPopup();
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
auto hide_popup() -> void {
|
||||
is_active = false;
|
||||
self.QComboBox::hidePopup();
|
||||
update_label_position();
|
||||
self.update();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
auto update_component_status() -> void {
|
||||
if (is_update_component_status) return;
|
||||
|
||||
auto font = self.font();
|
||||
font.setPixelSize(measurements.standard_font_height);
|
||||
self.setFont(font);
|
||||
standard_text_font = self.font();
|
||||
standard_text_font.setPixelSize(measurements.standard_font_height);
|
||||
|
||||
is_update_component_status = true;
|
||||
}
|
||||
|
||||
auto update_label_position() -> void {
|
||||
if ((is_focused || is_active) && self.currentIndex() != -1) {
|
||||
label_position->transition_to(1.0);
|
||||
} else if (is_focused || is_active) {
|
||||
label_position->transition_to(1.0);
|
||||
} else {
|
||||
label_position->transition_to(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
auto sync_basic_text_style(const QColor& text, const QColor& background,
|
||||
const QColor& selection_text, const QColor& selection_background,
|
||||
const QColor& hover) -> void {
|
||||
constexpr auto to_rgba = [](const QColor& color) {
|
||||
return QStringLiteral("rgba(%1, %2, %3, %4)")
|
||||
.arg(color.red())
|
||||
.arg(color.green())
|
||||
.arg(color.blue())
|
||||
.arg(color.alpha());
|
||||
};
|
||||
|
||||
constexpr auto kQComboBoxStyle = R"(
|
||||
QComboBox QAbstractItemView {
|
||||
border: none;
|
||||
background-color: %1;
|
||||
outline: none;
|
||||
}
|
||||
QComboBox QAbstractItemView::item {
|
||||
padding: 3px 5px;
|
||||
color: %2;
|
||||
}
|
||||
QComboBox QAbstractItemView::item:selected {
|
||||
background-color: %3;
|
||||
color: %4;
|
||||
}
|
||||
QComboBox QAbstractItemView::item:hover {
|
||||
background-color: %5;
|
||||
color: %6;
|
||||
}
|
||||
)";
|
||||
|
||||
const auto qss = QString {kQComboBoxStyle};
|
||||
self.setStyleSheet(qss.arg(to_rgba(background))
|
||||
.arg(to_rgba(text))
|
||||
.arg(to_rgba(selection_background))
|
||||
.arg(to_rgba(selection_text))
|
||||
.arg(to_rgba(hover))
|
||||
.arg(to_rgba(text)));
|
||||
// self.setStyleSheet(qss.arg(to_rgba(text))
|
||||
// .arg(to_rgba(background))
|
||||
// .arg(to_rgba(selection_text))
|
||||
// .arg(to_rgba(selection_background)));
|
||||
}
|
||||
auto get_color_tokens() const -> ColorSpace::Tokens const& {
|
||||
return is_disable ? color_specs.disabled
|
||||
: is_error ? color_specs.error
|
||||
: is_active ? color_specs.focused
|
||||
: is_focused ? color_specs.focused
|
||||
: color_specs.enabled;
|
||||
}
|
||||
|
||||
auto filled_line_width() const -> double {
|
||||
constexpr auto normal_width = 1;
|
||||
constexpr auto active_width = 3;
|
||||
return (is_focused && !is_disable) ? active_width : normal_width;
|
||||
}
|
||||
|
||||
constexpr auto measure_text(const QFont& font, const QString& text, const QTextOption& options)
|
||||
-> double {
|
||||
const auto fm = QFontMetricsF(font);
|
||||
const auto size = fm.size(Qt::TextSingleLine, text);
|
||||
return size.width();
|
||||
}
|
||||
private:
|
||||
Measurements measurements;
|
||||
ColorSpace color_specs;
|
||||
bool is_disable = false;
|
||||
bool is_hovered = false;
|
||||
bool is_focused = false;
|
||||
bool is_error = false;
|
||||
bool is_update_component_status = false;
|
||||
bool is_active = false;
|
||||
|
||||
QString label_text;
|
||||
|
||||
QIcon leading_icon;
|
||||
QString leading_icon_code;
|
||||
QString leading_font_name;
|
||||
|
||||
QFont leading_icon_font;
|
||||
QFont standard_text_font;
|
||||
|
||||
Animatable animatable;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> label_position;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> rotation_angle;
|
||||
|
||||
BasicSelect& self;
|
||||
};
|
||||
|
||||
#endif //TOUCHSENSOR_SELECT_IMPL_HH
|
||||
42
modern-qt/widget/shape/ellipse.hh
Normal file
42
modern-qt/widget/shape/ellipse.hh
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/painter/helper.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-qt/widget/shape/shape.hh"
|
||||
#include "modern-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
modern-qt/widget/shape/rounded-rect.hh
Normal file
101
modern-qt/widget/shape/rounded-rect.hh
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/painter/helper.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-qt/utility/wrapper/widget.hh"
|
||||
#include "modern-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
modern-qt/widget/shape/shape.hh
Normal file
23
modern-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
modern-qt/widget/shape/wave-circle.hh
Normal file
128
modern-qt/widget/shape/wave-circle.hh
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/solution/round-angle.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-qt/widget/shape/shape.hh"
|
||||
#include "modern-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
modern-qt/widget/sliders.cc
Normal file
37
modern-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);
|
||||
Widget::mousePressEvent(event);
|
||||
}
|
||||
auto Slider::mouseReleaseEvent(QMouseEvent* event) -> void {
|
||||
pimpl->mouse_release_event(event);
|
||||
Widget::mouseReleaseEvent(event);
|
||||
}
|
||||
auto Slider::mouseMoveEvent(QMouseEvent* event) -> void {
|
||||
pimpl->mouse_move_event(event);
|
||||
Widget::mouseMoveEvent(event);
|
||||
}
|
||||
auto Slider::paintEvent(QPaintEvent* event) -> void { pimpl->paint_event(event); }
|
||||
140
modern-qt/widget/sliders.hh
Normal file
140
modern-qt/widget/sliders.hh
Normal file
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/widget/widget.hh"
|
||||
|
||||
namespace creeper::slider::internal {
|
||||
|
||||
class Slider : public Widget {
|
||||
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;
|
||||
|
||||
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
modern-qt/widget/sliders.impl.hh
Normal file
254
modern-qt/widget/sliders.impl.hh
Normal file
@@ -0,0 +1,254 @@
|
||||
#include "sliders.hh"
|
||||
|
||||
#include "modern-qt/utility/animation/animatable.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-qt/utility/painter/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
modern-qt/widget/switch.cc
Normal file
63
modern-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
modern-qt/widget/switch.hh
Normal file
119
modern-qt/widget/switch.hh
Normal file
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-qt/utility/wrapper/pimpl.hh"
|
||||
#include "modern-qt/utility/wrapper/property.hh"
|
||||
#include "modern-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
modern-qt/widget/switch.impl.hh
Normal file
204
modern-qt/widget/switch.impl.hh
Normal file
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
#include "switch.hh"
|
||||
|
||||
#include <qpainter.h>
|
||||
|
||||
#include "modern-qt/utility/animation/animatable.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/state/spring.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-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
modern-qt/widget/text-fields.cc
Normal file
73
modern-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);
|
||||
}
|
||||
139
modern-qt/widget/text-fields.hh
Normal file
139
modern-qt/widget/text-fields.hh
Normal file
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/qt_wrapper/enter_event.hh"
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-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 <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;
|
||||
};
|
||||
|
||||
}
|
||||
469
modern-qt/widget/text-fields.impl.hh
Normal file
469
modern-qt/widget/text-fields.impl.hh
Normal file
@@ -0,0 +1,469 @@
|
||||
#include "text-fields.hh"
|
||||
|
||||
#include "modern-qt/utility/animation/animatable.hh"
|
||||
#include "modern-qt/utility/animation/state/pid.hh"
|
||||
#include "modern-qt/utility/animation/transition.hh"
|
||||
#include "modern-qt/utility/painter/common.hh"
|
||||
#include "modern-qt/utility/painter/container.hh"
|
||||
#include "modern-qt/utility/painter/helper.hh"
|
||||
#include "modern-qt/utility/painter/shape.hh"
|
||||
|
||||
#include <qpainter.h>
|
||||
#include <qpainterpath.h>
|
||||
|
||||
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 rect = self.rect();
|
||||
const auto color = get_color_tokens();
|
||||
|
||||
constexpr auto container_radius = 5;
|
||||
|
||||
update_component_status(FieldType::FILLED);
|
||||
|
||||
auto painter = QPainter { &self };
|
||||
// Container
|
||||
{
|
||||
util::PainterHelper { painter }
|
||||
.set_render_hint(QPainter::Antialiasing)
|
||||
.rounded_rectangle(color.container, Qt::transparent, 0, rect, container_radius,
|
||||
container_radius, 0, 0);
|
||||
}
|
||||
// Active Indicator
|
||||
{
|
||||
const auto p0 = rect.bottomLeft();
|
||||
const auto p1 = rect.bottomRight();
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen { color.active_indicator, filled_line_width() });
|
||||
painter.drawLine(p0, p1);
|
||||
}
|
||||
// Leading icon
|
||||
|
||||
if (!leading_icon.isNull()) {
|
||||
//
|
||||
} else if (!leading_icon_code.isEmpty()) {
|
||||
|
||||
const auto rect = QRectF {
|
||||
1.0 * measurements.row_padding_with_icons,
|
||||
0.5 * (measurements.container_height - measurements.icon_rect_size),
|
||||
1.0 * measurements.icon_rect_size,
|
||||
1.0 * measurements.icon_rect_size,
|
||||
};
|
||||
|
||||
painter.save();
|
||||
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen { color.leading_icon });
|
||||
painter.setFont(leading_icon_font);
|
||||
painter.drawText(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();
|
||||
// qDebug() << "margins = " << margins;
|
||||
const auto center_label_padding =
|
||||
0.5 * (measurements.container_height - measurements.icon_rect_size);
|
||||
const auto rect_center = QRectF {
|
||||
QPointF(margins.left(), center_label_padding),
|
||||
QPointF(self.width() - margins.right(), self.height() - center_label_padding),
|
||||
};
|
||||
const auto rect_top = QRectF {
|
||||
QPointF(margins.left(), measurements.col_padding),
|
||||
QPointF(self.width() - margins.right(), margins.top()),
|
||||
};
|
||||
|
||||
const auto 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, 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,
|
||||
{ static_cast<qreal>(measurements.row_padding_with_icons), 0 } },
|
||||
Paint::Text {
|
||||
TextOption { Qt::AlignCenter },
|
||||
Size { leading_box_size },
|
||||
Font { leading_icon_font },
|
||||
Text { leading_icon_code },
|
||||
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;
|
||||
|
||||
QFont leading_icon_font;
|
||||
QFont standard_text_font;
|
||||
|
||||
Animatable animatable;
|
||||
std::unique_ptr<TransitionValue<PidState<double>>> label_position;
|
||||
|
||||
BasicTextField& self;
|
||||
};
|
||||
57
modern-qt/widget/text.hh
Normal file
57
modern-qt/widget/text.hh
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "modern-qt/utility/theme/theme.hh"
|
||||
#include "modern-qt/utility/wrapper/common.hh"
|
||||
#include "modern-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); }>;
|
||||
|
||||
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
modern-qt/widget/widget.cc
Normal file
3
modern-qt/widget/widget.cc
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "widget.hh"
|
||||
|
||||
using namespace creeper;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user