diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9fd048e --- /dev/null +++ b/CMakeLists.txt @@ -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 +) \ No newline at end of file diff --git a/cmake/creeper-qtConfig.cmake.in b/cmake/creeper-qtConfig.cmake.in new file mode 100644 index 0000000..b8cdf80 --- /dev/null +++ b/cmake/creeper-qtConfig.cmake.in @@ -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) diff --git a/component.hh b/component.hh new file mode 100644 index 0000000..4f63f3b --- /dev/null +++ b/component.hh @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +template +using raw_pointer = T*; + +struct NavComponentState { + creeper::ThemeManager& manager; + std::function switch_callback; + + std::vector> buttons_context; +}; + +auto NavComponent(NavComponentState&) noexcept -> raw_pointer; + +struct ViewComponentState { + creeper::ThemeManager& manager; +}; +auto ViewComponent(ViewComponentState&) noexcept -> raw_pointer; \ No newline at end of file diff --git a/components/charts/heatmap.cc b/components/charts/heatmap.cc new file mode 100644 index 0000000..4e91f4a --- /dev/null +++ b/components/charts/heatmap.cc @@ -0,0 +1,12 @@ +// +// Created by Lenn on 2025/10/17. +// + +#include "heatmap.hh" +#include "heatmap.impl.hh" + +BasicPlot::BasicPlot() + : pimpl {std::make_unique(*this)} {} + +void BasicPlot::set_matrix_size(const QSize &) { +} diff --git a/components/charts/heatmap.hh b/components/charts/heatmap.hh new file mode 100644 index 0000000..96e9be7 --- /dev/null +++ b/components/charts/heatmap.hh @@ -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; + using XLabelText = common::pro::String; + using YLabelText = common::pro::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 \ No newline at end of file diff --git a/components/charts/heatmap.impl.hh b/components/charts/heatmap.impl.hh new file mode 100644 index 0000000..8d9975c --- /dev/null +++ b/components/charts/heatmap.impl.hh @@ -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 \ No newline at end of file diff --git a/components/nav.cc b/components/nav.cc new file mode 100644 index 0000000..773c073 --- /dev/null +++ b/components/nav.cc @@ -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 { + + 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 { + ln::Spacing {10}, + ln::Margin {15}, + ln::Item { + {0, Qt::AlignHCenter}, + AvatarComponent, + }, + ln::SpacingItem {20}, + ln::Item> { + {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 { + {0, Qt::AlignHCenter}, + navigation_icons_config, + ic::TypesDefault, + ic::FontIcon {material::icon::kLogout}, + ic::Clickable {&app::quit}, + }, + ln::Item { + {0, Qt::AlignHCenter}, + navigation_icons_config, + ic::ColorFilled, + ic::FontIcon {material::icon::kDarkMode}, + ic::Clickable{[&]{state.manager.toggle_color_mode();state.manager.apply_theme();}}, + } + } + }; +} + diff --git a/components/view.cc b/components/view.cc new file mode 100644 index 0000000..964ddc6 --- /dev/null +++ b/components/view.cc @@ -0,0 +1,178 @@ +// +// Created by Lenn on 2025/10/14. +// + +#include + +#include "component.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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>(); + slogen_context->set_silent("BanG Bream! It's MyGo!!!"); + + // 创建一个MutableValue来存储MatSelect的选项 + auto select_com_context = std::make_shared>(); + select_com_context->set_silent(QStringList {}); + + auto select_baud_context = std::make_shared>(); + select_baud_context->set_silent(QStringList {"9600", "115200"}); + + const auto row = new Row { + // lnpro::Item { + // text_field::pro::ThemeManager {manager}, + // text_field::pro::LeadingIcon {material::icon::kSearch, material::regular::font}, + // MutableForward { + // text_field::pro::LabelText {}, + // slogen_context, + // }, + // }, + lnpro::Item { + slpro::ThemeManager {manager}, + slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font}, + slpro::IndexChanged {[&](auto& self){ qDebug() << self.currentIndex();}}, + slpro::LeadingText {"COM"}, + }, + lnpro::Item { + 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 { + // // slpro::ThemeManager {manager}, + // // slpro::LeadingIcon {material::icon::kArrowDropDown, material::regular::font}, + // // slpro::IndexChanged {} + // // } + lnpro::SpacingItem {20}, + lnpro::Item { + 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 { + 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 { + const auto texts = std::array { + std::make_shared>("0.500"), + std::make_shared>("0.500"), + std::make_shared>("0.500"), + }; + const auto progresses = std::array { + std::make_shared>(0.5), + std::make_shared>(0.5), + std::make_shared>(0.5), + }; + return new FilledCard { + capro::ThemeManager { state.manager }, + capro::SizePolicy {QSizePolicy::Expanding}, + capro::Layout { + 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 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); + } + }), + }, + }, + }; +} \ No newline at end of file diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..4a4d986 Binary files /dev/null and b/images/logo.png differ diff --git a/main.cc b/main.cc new file mode 100644 index 0000000..8a39435 --- /dev/null +++ b/main.cc @@ -0,0 +1,87 @@ +#include "component.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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& window) noexcept { + + }, + mwpro::FixedSize {1080, 720}, + mwpro::Central { + capro::ThemeManager {manager}, + capro::Radius {0}, + capro::Level {CardLevel::HIGHEST}, + + capro::Layout { + lnpro::Margin{0}, + lnpro::Spacing{0}, + lnpro::Item { + NavComponent(nav_component_state), + }, + lnpro::Item { + lnpro::ContentsMargin{{15, 15, 15, 15}}, + }, + lnpro::Item { + {255}, + lnpro::ContentsMargin { { 5, 15, 15, 15 } }, + lnpro::Item { + 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(); +} \ No newline at end of file diff --git a/modern-qt/core.hh b/modern-qt/core.hh new file mode 100644 index 0000000..90330be --- /dev/null +++ b/modern-qt/core.hh @@ -0,0 +1 @@ +#include "core/application.hh" \ No newline at end of file diff --git a/modern-qt/core/application.hh b/modern-qt/core/application.hh new file mode 100644 index 0000000..3327b16 --- /dev/null +++ b/modern-qt/core/application.hh @@ -0,0 +1,65 @@ +#pragma once + +#include "modern-qt/utility/wrapper/common.hh" +#include "modern-qt/utility/wrapper/property.hh" +#include +#include + +namespace creeper::app::pro { + +using Token = common::Token; + +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 +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); +} +namespace creeper::app { + +struct Application { }; +using init = Declarative; + +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 + +} diff --git a/modern-qt/creeper-qt.hh b/modern-qt/creeper-qt.hh new file mode 100644 index 0000000..8ba0681 --- /dev/null +++ b/modern-qt/creeper-qt.hh @@ -0,0 +1,4 @@ +#include "core.hh" +#include "layout.hh" +#include "utility.hh" +#include "widget.hh" diff --git a/modern-qt/layout.hh b/modern-qt/layout.hh new file mode 100644 index 0000000..5526347 --- /dev/null +++ b/modern-qt/layout.hh @@ -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" diff --git a/modern-qt/layout/flow.cc b/modern-qt/layout/flow.cc new file mode 100644 index 0000000..e9d77fd --- /dev/null +++ b/modern-qt/layout/flow.cc @@ -0,0 +1,156 @@ +#include "flow.hh" +#include + +using namespace creeper::flow::internal; + +struct Flow::Impl { + QList items; + + int row_spacing = 10; + int col_spacing = 10; + + int row_limit = std::numeric_limits::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(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } else { + return static_cast(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(*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; } diff --git a/modern-qt/layout/flow.hh b/modern-qt/layout/flow.hh new file mode 100644 index 0000000..6504fb1 --- /dev/null +++ b/modern-qt/layout/flow.hh @@ -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 + +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; + +using RowSpacing = SetterProp; + +using ColSpacing = SetterProp; + +using RowLimit = SetterProp; + +using MainAxisSpacing = RowSpacing; +using CrossAxisSpacing = ColSpacing; +using MaxItemsInEachRow = RowLimit; + +template +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); +using namespace layout::pro; +} +namespace creeper { + +using Flow = Declarative>; + +} diff --git a/modern-qt/layout/form.hh b/modern-qt/layout/form.hh new file mode 100644 index 0000000..e69de29 diff --git a/modern-qt/layout/grid.hh b/modern-qt/layout/grid.hh new file mode 100644 index 0000000..813dd0b --- /dev/null +++ b/modern-qt/layout/grid.hh @@ -0,0 +1,83 @@ +#pragma once + +#include "modern-qt/utility/wrapper/common.hh" +#include "modern-qt/utility/wrapper/property.hh" + +#include + +namespace creeper { +namespace grid::internal { + class Grid : public QGridLayout { }; +} +namespace grid::pro { + using Token = common::Token; + + struct RowSpacing : Token { }; + + struct ColSpacing : Token { }; + + template + requires std::is_convertible_v || std::is_convertible_v + 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 + : item_pointer { new T { std::forward(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) + layout.addWidget(item_pointer, method.row, method.col, method.align); + if constexpr (std::is_convertible_v) + layout.addLayout(item_pointer, method.row, method.col, method.align); + } else { + if constexpr (std::is_convertible_v) + layout.addWidget(item_pointer, method.row, method.row_span, method.col, + method.col_span, method.align); + if constexpr (std::is_convertible_v) + 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 + concept trait = std::derived_from; + + CREEPER_DEFINE_CHECKER(trait); + +} +using Grid = Declarative; +} diff --git a/modern-qt/layout/group.hh b/modern-qt/layout/group.hh new file mode 100644 index 0000000..3bf13d9 --- /dev/null +++ b/modern-qt/layout/group.hh @@ -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 +concept foreach_invoke_item_trait = requires { + { std::invoke(std::declval(), std::declval()) } -> widget_pointer_trait; +}; + +template +concept foreach_apply_item_trait = requires { + { std::apply(std::declval(), std::declval()) } -> widget_pointer_trait; +}; + +template +concept foreach_item_trait = foreach_invoke_item_trait || foreach_apply_item_trait; + +template +concept foreach_invoke_ranges_trait = foreach_item_trait>; + +template +struct Group : public T { + using T::T; + std::vector widgets; + + template + requires foreach_invoke_ranges_trait + 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) + widget_pointer = std::invoke(f, item); + + else if constexpr (foreach_apply_item_trait) + 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 + { + for (auto widget : self.widgets) + std::invoke(f, *widget); + } +}; + +}; + +namespace creeper::group::pro { + +using Token = common::Token>; + +/// @note +/// 一种典型的用法,委托构造时,所传函数只能接受常量引用, +/// 放心使用 auto,类型是可以被推导出来的 +/// +/// group::pro::Compose { +/// std::array { +/// std::tuple(1, "xxxxxx"), +/// ...... +/// }, +/// [](auto index, auto text) { +/// return new TextButton { ... }; +/// }, +/// } +/// +template + requires internal::foreach_invoke_ranges_trait +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 + requires(!std::invocable) +struct Foreach : Token { + F function; + + explicit Foreach(F&& f) noexcept + : function { std::forward(f) } { } + + auto apply(auto& self) const noexcept { + // 很遗憾,Qt 占用了 foreach 这个单词 + self.foreach_(std::move(function)); + } +}; + +template +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait) +}; +namespace creeper { + +template +using Group = + Declarative, CheckerOr>; + +} diff --git a/modern-qt/layout/linear.hh b/modern-qt/layout/linear.hh new file mode 100644 index 0000000..624c3a0 --- /dev/null +++ b/modern-qt/layout/linear.hh @@ -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 + +namespace creeper::linear::pro { + +using Token = common::Token; + +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 { +/// { 0, Qt::AlignHCenter } // stretch, and alignment, optional +/// ... +/// }; +/// +template +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 + : item_pointer { new T { std::forward(args)... } } + , method(method) { } + + explicit Item(auto&&... args) noexcept + requires std::constructible_from + : item_pointer { new T { std::forward(args)... } } { } + + void apply(linear_trait auto& layout) const { + if constexpr (widget_trait) layout.addWidget(item_pointer, method.stretch, method.align); + if constexpr (layout_trait) layout.addLayout(item_pointer, method.stretch); + } +}; + +template +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); +using namespace layout::pro; +} + +namespace creeper { + +template +using BoxLayout = Declarative>; + +using Row = BoxLayout; +using Col = BoxLayout; + +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 {}; + } +} + +} diff --git a/modern-qt/layout/mixer.cc b/modern-qt/layout/mixer.cc new file mode 100644 index 0000000..82847a6 --- /dev/null +++ b/modern-qt/layout/mixer.cc @@ -0,0 +1,3 @@ +#include "mixer.hh" + +using namespace creeper::mixer::internal; diff --git a/modern-qt/layout/mixer.hh b/modern-qt/layout/mixer.hh new file mode 100644 index 0000000..f36c00c --- /dev/null +++ b/modern-qt/layout/mixer.hh @@ -0,0 +1,112 @@ +#pragma once + +#include +#include + +#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>(); + + 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>> 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; + +} diff --git a/modern-qt/layout/mutual-exclusion-group.hh b/modern-qt/layout/mutual-exclusion-group.hh new file mode 100644 index 0000000..d2d7891 --- /dev/null +++ b/modern-qt/layout/mutual-exclusion-group.hh @@ -0,0 +1,98 @@ +#pragma once +#include +#include +#include + +namespace creeper::mutual_exclusion_group::internal { + +template +concept switch_function_trait = std::invocable; + +template + requires switch_function_trait +struct MutualExclusionGroup : public Group { + using Group::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; + +template +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 +concept trait = std::derived_from || group::pro::trait; + +CREEPER_DEFINE_CHECKER(trait); +using namespace group::pro; +} +namespace creeper { + +template + requires mutual_exclusion_group::internal::switch_function_trait +using MutualExclusionGroup = + mutual_exclusion_group::internal::MutualExclusionGroup; + +template +using CheckGroup = Declarative< + MutualExclusionGroup, + CheckerOr>; +namespace check_group = mutual_exclusion_group; + +template +using OpenGroup = Declarative< + MutualExclusionGroup, + CheckerOr>; +namespace open_group = mutual_exclusion_group; + +template +using SelectGroup = Declarative< + MutualExclusionGroup, + CheckerOr>; +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 {}; + } +} + +} diff --git a/modern-qt/layout/scroll.hh b/modern-qt/layout/scroll.hh new file mode 100644 index 0000000..b1ccdaf --- /dev/null +++ b/modern-qt/layout/scroll.hh @@ -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 +#include + +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; + +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 +struct Item : Token { + T* item_pointer = nullptr; + + explicit Item(auto&&... args) noexcept + requires std::constructible_from + : item_pointer { new T { std::forward(args)... } } { } + + explicit Item(T* pointer) noexcept + : item_pointer { pointer } { } + + auto apply(area_trait auto& layout) const noexcept -> void { + if constexpr (widget_trait) { + layout.setWidget(item_pointer); + } + // NOTE: 这里可能有调整的空间,直接设置 Layout, + // 布局 Size 行为是不正确的 + else if constexpr (layout_trait) { + const auto content = new Widget { + widget::pro::Layout { item_pointer }, + }; + layout.setWidget(content); + } + } +}; + +template +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); +using namespace widget::pro; +using namespace theme::pro; +} +namespace creeper { + +using ScrollArea = Declarative>; + +} diff --git a/modern-qt/layout/stacked.hh b/modern-qt/layout/stacked.hh new file mode 100644 index 0000000..80ec630 --- /dev/null +++ b/modern-qt/layout/stacked.hh @@ -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 + +namespace creeper::stacked::internal { +class Stacked : public QStackedLayout { }; +} + +namespace creeper::stacked::pro { + +using Token = common::Token; + +template +concept trait = std::derived_from || layout::pro::trait; + +CREEPER_DEFINE_CHECKER(trait); +using namespace layout::pro; +} + +namespace creeper { +using Stacked = Declarative; +using NavHost = Stacked; +} diff --git a/modern-qt/utility.hh b/modern-qt/utility.hh new file mode 100644 index 0000000..ff73653 --- /dev/null +++ b/modern-qt/utility.hh @@ -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" diff --git a/modern-qt/utility/animation/animatable.cc b/modern-qt/utility/animation/animatable.cc new file mode 100644 index 0000000..f767d8a --- /dev/null +++ b/modern-qt/utility/animation/animatable.cc @@ -0,0 +1,54 @@ +#include "animatable.hh" +#include +using namespace creeper; + +#include +using qwidget = QWidget; +using qtimer = QTimer; + +struct Animatable::Impl { + + qwidget& component; + qtimer scheduler; + + std::vector> 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 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& 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(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 task) noexcept -> void { + pimpl->push_transition_task(std::move(task)); +} diff --git a/modern-qt/utility/animation/animatable.hh b/modern-qt/utility/animation/animatable.hh new file mode 100644 index 0000000..401e7da --- /dev/null +++ b/modern-qt/utility/animation/animatable.hh @@ -0,0 +1,27 @@ +#pragma once + +#include "modern-qt/utility/wrapper/pimpl.hh" +#include + +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 task) noexcept -> void; +}; + +} diff --git a/modern-qt/utility/animation/math.hh b/modern-qt/utility/animation/math.hh new file mode 100644 index 0000000..dc5d5dc --- /dev/null +++ b/modern-qt/utility/animation/math.hh @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include + +namespace creeper::animate { + +template +constexpr auto zero() noexcept { + if constexpr (std::is_arithmetic_v) { + 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 +constexpr auto magnitude(const T& error) noexcept { + if constexpr (std::is_arithmetic_v) { + 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 +constexpr auto normalize(const T& error) noexcept { + if constexpr (std::is_arithmetic_v) { + return error; + } else if constexpr (requires { error.norm(); }) { + return error.norm(); + } else { + static_assert(sizeof(T) == 0, "magnitude() not implemented for this type"); + } +} + +template +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) { + return static_cast(start + (end - start) * clamped_t); + } else if constexpr ( // + requires(const T& a, const T& b, const double f) { + { a - b } -> std::convertible_to; + { a * f } -> std::convertible_to; + { a + b } -> std::convertible_to; + }) { + 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); +} + +} diff --git a/modern-qt/utility/animation/state/accessor.hh b/modern-qt/utility/animation/state/accessor.hh new file mode 100644 index 0000000..1fa9ac9 --- /dev/null +++ b/modern-qt/utility/animation/state/accessor.hh @@ -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; } +}; + +} diff --git a/modern-qt/utility/animation/state/linear.hh b/modern-qt/utility/animation/state/linear.hh new file mode 100644 index 0000000..7eab96a --- /dev/null +++ b/modern-qt/utility/animation/state/linear.hh @@ -0,0 +1,75 @@ +#pragma once +#include "modern-qt/utility/animation/math.hh" +#include "modern-qt/utility/animation/state/accessor.hh" + +#include + +namespace creeper { + +template +struct LinearState : public NormalAccessor { + using ValueT = T; + + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + + T value = animate::zero(); + T target = animate::zero(); + + 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(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; + } +}; + +} diff --git a/modern-qt/utility/animation/state/pid.hh b/modern-qt/utility/animation/state/pid.hh new file mode 100644 index 0000000..253d0cf --- /dev/null +++ b/modern-qt/utility/animation/state/pid.hh @@ -0,0 +1,94 @@ +#pragma once +#include "modern-qt/utility/animation/math.hh" +#include "modern-qt/utility/animation/state/accessor.hh" + +#include + +namespace creeper { + +template +struct PidState : public NormalAccessor { + using ValueT = T; + + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + + T value = animate::zero(); + T target = animate::zero(); + + 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 last_error = animate::zero(); + 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(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(); + details.last_error = animate::zero(); + 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; + } +}; + +} diff --git a/modern-qt/utility/animation/state/spring.hh b/modern-qt/utility/animation/state/spring.hh new file mode 100644 index 0000000..89c0843 --- /dev/null +++ b/modern-qt/utility/animation/state/spring.hh @@ -0,0 +1,72 @@ +#pragma once +#include "modern-qt/utility/animation/math.hh" +#include "modern-qt/utility/animation/state/accessor.hh" + +#include + +namespace creeper { + +template +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(); + + 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(); + 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(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(); + return !done; + } +}; +} diff --git a/modern-qt/utility/animation/transition.hh b/modern-qt/utility/animation/transition.hh new file mode 100644 index 0000000..344fa0f --- /dev/null +++ b/modern-qt/utility/animation/transition.hh @@ -0,0 +1,86 @@ +#pragma once +#include "animatable.hh" + +namespace creeper { + +template +concept transition_state_trait = requires(T& t) { + typename T::ValueT; + { t.get_value() } -> std::same_as; + { t.get_target() } -> std::same_as; + + { t.set_value(std::declval()) }; + { t.set_target(std::declval()) }; + + { t.update() } -> std::same_as; +}; + +// Functor like lambda +template +struct TransitionTask : public ITransitionTask { +public: + explicit TransitionTask(std::shared_ptr state, std::shared_ptr 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; + std::shared_ptr running; +}; + +template +struct TransitionValue { +public: + using T = State::ValueT; + + explicit TransitionValue(Animatable& animatable, std::shared_ptr 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(true); + + // Push new transition task + auto task = std::make_unique>(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; + std::shared_ptr running; + + Animatable& animatable; +}; + +template +inline auto make_transition(Animatable& core, std::shared_ptr state) { + return std::make_unique>(core, state); +} + +} diff --git a/modern-qt/utility/animation/water-ripple.hh b/modern-qt/utility/animation/water-ripple.hh new file mode 100644 index 0000000..367d632 --- /dev/null +++ b/modern-qt/utility/animation/water-ripple.hh @@ -0,0 +1,71 @@ +#pragma once + +#include "modern-qt/utility/animation/state/accessor.hh" +#include "modern-qt/utility/animation/transition.hh" + +#include +#include +#include +#include +#include +#include + +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(); + 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>> ripples; + Animatable& animatable; + double speed; +}; + +} diff --git a/modern-qt/utility/content-scale.hh b/modern-qt/utility/content-scale.hh new file mode 100644 index 0000000..328dc89 --- /dev/null +++ b/modern-qt/utility/content-scale.hh @@ -0,0 +1,64 @@ +#pragma once +#include +#include + +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(size.width()); + auto target_height = static_cast(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 {}; + } +}; + +} diff --git a/modern-qt/utility/material-icon.hh b/modern-qt/utility/material-icon.hh new file mode 100644 index 0000000..d41e8a4 --- /dev/null +++ b/modern-qt/utility/material-icon.hh @@ -0,0 +1,166 @@ +#pragma once + +#include +#include + +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"; + } +} +} diff --git a/modern-qt/utility/painter-resource.hh b/modern-qt/utility/painter-resource.hh new file mode 100644 index 0000000..f2ecedd --- /dev/null +++ b/modern-qt/utility/painter-resource.hh @@ -0,0 +1,122 @@ +#pragma once +#include +#include +#include + +namespace creeper { + +namespace painter_resource { + template + concept finished_callback_c = std::invocable || std::invocable; +} + +struct PainterResource : public QPixmap { + + constexpr explicit PainterResource(std::string_view url) noexcept + : QPixmap {} { + const auto qurl = QUrl(QString::fromUtf8(url.data(), static_cast(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 + { + const auto qurl = QUrl(QString::fromUtf8(url.data(), static_cast(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 + explicit PainterResource(T&& other) noexcept + requires std::convertible_to + : QPixmap(std::forward(other)) { } + + template + auto operator=(T&& other) noexcept -> PainterResource& + requires std::convertible_to + { + QPixmap::operator=(std::forward(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 auto&& f) { + finished_callback_ = std::forward(f); + } + +private: + std::optional> finished_callback_; + + bool is_loading_ = false; + bool is_error_ = false; + + std::shared_ptr resource_exiting = std::make_shared(true); + + auto download_resource_from_network(const QUrl& url, auto&& f) noexcept -> void + requires painter_resource::finished_callback_c + { + 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) std::invoke(f, *this); + if constexpr (std::invocable) 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://"); + } +}; + +} diff --git a/modern-qt/utility/painter/common.hh b/modern-qt/utility/painter/common.hh new file mode 100644 index 0000000..f6923e5 --- /dev/null +++ b/modern-qt/utility/painter/common.hh @@ -0,0 +1,98 @@ +#pragma once +#include "modern-qt/utility/wrapper/property.hh" + +#include + +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 +concept common_trait = requires(T t) { + { auto { t.origin } } -> std::same_as; + { auto { t.size } } -> std::same_as; +}; + +template +concept container_trait = requires(T t) { + { auto { t.align } } -> std::same_as; +} && common_trait; + +template +concept shape_trait = requires(T t) { + { auto { t.color_container } } -> std::same_as; + { auto { t.color_outline } } -> std::same_as; + { auto { t.thickness_outline } } -> std::same_as; +}; + +template +concept drawable_trait = common_trait && std::invocable; + +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; +using Origin = DerivedProp; + +using ContainerColor = + SetterProp; +using OutlineColor = + SetterProp; +using OutlineWidth = + SetterProp; + +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 +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); +} diff --git a/modern-qt/utility/painter/container.hh b/modern-qt/utility/painter/container.hh new file mode 100644 index 0000000..12a6846 --- /dev/null +++ b/modern-qt/utility/painter/container.hh @@ -0,0 +1,347 @@ +#pragma once +#include "modern-qt/utility/painter/common.hh" + +namespace creeper::painter { + +// 核心容器结构体,现在继承自 Impl,使其满足 drawable_trait (假设 Impl 继承了所需的属性) +template +struct Container : public Impl { + std::tuple...> drawable; + + // 唯一构造函数:接受 Impl 实例和可变参数包 + constexpr explicit Container(const Impl& impl, Ts&&... drawable) + : Impl { impl } + , drawable { std::make_tuple(std::forward(drawable)...) } { } + + auto operator()(qt::painter& painter) + requires(std::invocable && ...) + { + render(painter); + } + auto render(qt::painter& painter) noexcept + requires(std::invocable && ...) + { + constexpr auto has_unique = requires { // + static_cast(*this).unique_render(painter, drawable); + }; + if constexpr (has_unique) { + static_cast(*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 + auto make_layout(this auto& self, std::tuple& 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 + auto unique_render(qt::painter& main_painter, std::tuple& 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 + auto make_layout(this auto& self, std::tuple& 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 + auto make_layout(this auto& self, std::tuple& 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 +Container(const SurfaceImpl& impl, Ts&&... args) -> Container; + +template +Container(const BufferImpl& impl, Ts&&... args) -> Container; + +template +Container(const BoxImpl& impl, Ts&&... args) -> Container; + +template +Container(const RowImpl& impl, Ts&&... args) -> Container; + +template +Container(const ColImpl& impl, Ts&&... args) -> Container; + +// ---------------------------------------------------------------------- +// Paint 类型导出 +// ---------------------------------------------------------------------- + +namespace Paint { + + template + using Surface = Container; + + template + using Buffer = Container; + + template + using Box = Container; + + template + using Row = Container; + + template + using Col = Container; +} + +} // namespace creeper::painter diff --git a/modern-qt/utility/painter/helper.cc b/modern-qt/utility/painter/helper.cc new file mode 100644 index 0000000..bf3047e --- /dev/null +++ b/modern-qt/utility/painter/helper.cc @@ -0,0 +1,15 @@ +#include "modern-qt/utility/painter/helper.hh" +#include + +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++; + } +} + +} diff --git a/modern-qt/utility/painter/helper.hh b/modern-qt/utility/painter/helper.hh new file mode 100644 index 0000000..f20449d --- /dev/null +++ b/modern-qt/utility/painter/helper.hh @@ -0,0 +1,224 @@ +#pragma once + +#include +#include + +namespace creeper::util { + +auto print_paint_event_count() noexcept -> void; + +/// @brief 隐藏冗杂的细节,解放命令式的绘图调用 +class PainterHelper { +public: + using Renderer = std::function; + + 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, 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; + } +}; +} diff --git a/modern-qt/utility/painter/shape.hh b/modern-qt/utility/painter/shape.hh new file mode 100644 index 0000000..64d5ba6 --- /dev/null +++ b/modern-qt/utility/painter/shape.hh @@ -0,0 +1,200 @@ +#pragma once +#include "modern-qt/utility/painter/common.hh" +#include + +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 { + 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; +using RadiusTR = SetterProp; +using RadiusBL = SetterProp; +using RadiusBR = SetterProp; +using Radiuses = SetterProp; + +/// Export Text +using Text = DerivedProp; +using Font = DerivedProp; +using Color = DerivedProp; +using Scale = SetterProp; + +using TextOption = DerivedProp; + +namespace Paint { + using EraseRectangle = Declarative>; + using Rectangle = Declarative>; + using RoundedRectangle = + Declarative>; + using Text = Declarative>; +} + +} diff --git a/modern-qt/utility/qt_wrapper/enter_event.hh b/modern-qt/utility/qt_wrapper/enter_event.hh new file mode 100644 index 0000000..fc3b9d5 --- /dev/null +++ b/modern-qt/utility/qt_wrapper/enter_event.hh @@ -0,0 +1,19 @@ +#pragma once + +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include +#else +#include +#endif + +namespace creeper::qt { + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +using EnterEvent = QEnterEvent; +#else +using EnterEvent = QEvent; +#endif + +} diff --git a/modern-qt/utility/qt_wrapper/margin_setter.hh b/modern-qt/utility/qt_wrapper/margin_setter.hh new file mode 100644 index 0000000..86b3f7b --- /dev/null +++ b/modern-qt/utility/qt_wrapper/margin_setter.hh @@ -0,0 +1,15 @@ +#pragma once + +#include + +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 +}; + +} diff --git a/modern-qt/utility/solution/round-angle.cc b/modern-qt/utility/solution/round-angle.cc new file mode 100644 index 0000000..545da4a --- /dev/null +++ b/modern-qt/utility/solution/round-angle.cc @@ -0,0 +1,54 @@ +#include "round-angle.hh" +#include + +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); +} diff --git a/modern-qt/utility/solution/round-angle.hh b/modern-qt/utility/solution/round-angle.hh new file mode 100644 index 0000000..d7c3e38 --- /dev/null +++ b/modern-qt/utility/solution/round-angle.hh @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +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; +}; + +} diff --git a/modern-qt/utility/theme/color-scheme.hh b/modern-qt/utility/theme/color-scheme.hh new file mode 100644 index 0000000..de21f6f --- /dev/null +++ b/modern-qt/utility/theme/color-scheme.hh @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +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; +}; + +} diff --git a/modern-qt/utility/theme/preset/blue-miku.hh b/modern-qt/utility/theme/preset/blue-miku.hh new file mode 100644 index 0000000..83f8edd --- /dev/null +++ b/modern-qt/utility/theme/preset/blue-miku.hh @@ -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, +}; + +} diff --git a/modern-qt/utility/theme/preset/gloden-harvest.hh b/modern-qt/utility/theme/preset/gloden-harvest.hh new file mode 100644 index 0000000..66c5bce --- /dev/null +++ b/modern-qt/utility/theme/preset/gloden-harvest.hh @@ -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, +}; + +} diff --git a/modern-qt/utility/theme/preset/green.hh b/modern-qt/utility/theme/preset/green.hh new file mode 100644 index 0000000..2991ba2 --- /dev/null +++ b/modern-qt/utility/theme/preset/green.hh @@ -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, +}; + +} diff --git a/modern-qt/utility/theme/theme.cc b/modern-qt/utility/theme/theme.cc new file mode 100644 index 0000000..7090770 --- /dev/null +++ b/modern-qt/utility/theme/theme.cc @@ -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 handlers; + std::vector begin_callbacks; + std::vector 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()) { } + +ThemeManager::ThemeManager(const ThemePack& pack, ColorMode mode) + : pimpl(std::make_unique()) { + 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); +} diff --git a/modern-qt/utility/theme/theme.hh b/modern-qt/utility/theme/theme.hh new file mode 100644 index 0000000..81230ca --- /dev/null +++ b/modern-qt/utility/theme/theme.hh @@ -0,0 +1,126 @@ +#pragma once + +#include + +// #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 + +namespace creeper::theme { + +class ThemeManager; + +template +concept color_scheme_setter_trait = requires(T t) { + { t.set_color_scheme(ColorScheme {}) }; +}; +template +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 pimpl; +public: + + explicit ThemeManager(const ThemePack& pack, ColorMode mode = ColorMode::LIGHT); + + void apply_theme() const; + + using Handler = std::function; + + /// 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; + +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 +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); + +} +namespace creeper { + +using ColorMode = theme::ColorMode; +using ColorScheme = theme::ColorScheme; +using ThemePack = theme::ThemePack; +using ThemeManager = theme::ThemeManager; + +} diff --git a/modern-qt/utility/trait/widget.hh b/modern-qt/utility/trait/widget.hh new file mode 100644 index 0000000..6d2a1eb --- /dev/null +++ b/modern-qt/utility/trait/widget.hh @@ -0,0 +1,40 @@ +#pragma once +#include +#include + +namespace creeper { + +template +concept widget_trait = std::convertible_to; + +template +concept widget_pointer_trait = std::convertible_to; + +template +concept layout_trait = std::convertible_to; + +template +concept layout_pointer_trait = std::convertible_to; + +template +concept item_trait = widget_trait || layout_trait; + +template +concept linear_trait = requires(T t) { + { t.addWidget(std::declval(), int {}, Qt::AlignCenter) }; + { t.addLayout(std::declval(), int {}) }; +}; + +template +concept area_trait = requires(T t) { + { t.setWidget(std::declval()) }; + { t.setLayout(std::declval()) }; +}; + +template +concept selectable_trait = requires(T t) { + { std::as_const(t).selected() } -> std::convertible_to; + { t.set_selected(bool {}) }; +}; + +} diff --git a/modern-qt/utility/wrapper/common.hh b/modern-qt/utility/wrapper/common.hh new file mode 100644 index 0000000..275c5f6 --- /dev/null +++ b/modern-qt/utility/wrapper/common.hh @@ -0,0 +1,215 @@ +#pragma once + +#include "property.hh" + +#include +#include +#include +#include +#include + +namespace creeper::common { +template +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 + using Opacity = SetterProp; + + // 设置圆角(NXNY) + template + using RadiusNxNy = + SetterProp; + + // 设置圆角(PXPY) + template + using RadiusPxPy = + SetterProp; + + // 设置圆角(NXPY) + template + using RadiusNxPy = + SetterProp; + + // 设置圆角(PXNY) + template + using RadiusPxNy = + SetterProp; + + // 设置圆角(X方向) + template + using RadiusX = SetterProp; + + // 设置圆角(Y方向) + template + using RadiusY = SetterProp; + + // 设置通用圆角 + template + using Radius = SetterProp; + + // 通用边界宽度 + template + using BorderWidth = + SetterProp; + + // 通用边界颜色 + template + using BorderColor = + SetterProp; + + // 通用文字颜色 + template + using TextColor = + SetterProp; + + // 通用背景颜色 + template + using Background = + SetterProp; + + // 通用水波纹颜色 + template + using WaterColor = + SetterProp; + + // 通用禁止属性 + template + using Disabled = SetterProp; + + // 通用 Checked 属性 + template + using Checked = SetterProp; + + // 通用文本属性 + template + 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 + struct Vector : public QVector, Token { + using QVector::QVector; + + explicit Vector(const QVector& vec) noexcept + : QVector { vec } { } + explicit Vector(const std::vector& vec) noexcept + : QVector { vec } { } + + void apply(auto& self) const + requires requires {setter(self, *this); } + { + setter(self, *this); + } + }; + + // template + // Vector::Vector(const QVector &vec) noexcept:QVector { vec } { } + + template + using Text = String; + + // 通用指针绑定 + template + struct Bind : Token { + Final*& widget; + explicit Bind(Final*& p) + : widget(p) { }; + void apply(Final& self) const { widget = &self; } + }; + + // 通用点击事件 + template + struct Clickable : Token { + Callback callback; + explicit Clickable(Callback callback) noexcept + : callback { std::move(callback) } { } + auto apply(auto& self) const noexcept -> void + requires std::invocable || std::invocable + { + using widget_t = std::remove_cvref_t; + QObject::connect(&self, &widget_t::clicked, [function = callback, &self] { + if constexpr (std::invocable) function(self); + if constexpr (std::invocable) function(); + }); + } + }; + + template + struct IndexChanged : Token { + Callback callback; + explicit IndexChanged(Callback callback) noexcept + : callback {std::move(callback)} {} + auto apply(auto& self) const noexcept -> void + requires std::invocable || std::invocable { + using widget_t = std::remove_cvref_t; + QObject::connect(&self, &widget_t::currentIndexChanged, [function = callback, &self] { + if constexpr (std::invocable) function(self); + }); + } + }; + + // 自定义信号回调注册 + + namespace internal { + template + struct FunctionArgs; + + template + struct FunctionArgsR> { + using type = std::tuple; + }; + template + struct FunctionArgsR> { + using type = std::tuple; + }; + + template + concept tuple_invocable_trait = requires(F&& f, Tuple&& t) { + std::apply(std::forward(f), std::forward(t)); // + }; + } + + template + struct SignalInjection : Token { + F f; + + using SignalArgs = typename internal::FunctionArgs::type; + + explicit SignalInjection(F f) noexcept + requires internal::tuple_invocable_trait + : f { std::forward(f) } { } + + auto apply(auto& self) const noexcept { QObject::connect(&self, signal, f); } + }; + +} +} diff --git a/modern-qt/utility/wrapper/layout.hh b/modern-qt/utility/wrapper/layout.hh new file mode 100644 index 0000000..68d2cb2 --- /dev/null +++ b/modern-qt/utility/wrapper/layout.hh @@ -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; + +using ContentsMargin = SetterProp; + +using Alignment = SetterProp; + +using Spacing = + SetterProp; + +using Margin = SetterProp; + +template +struct Widget : Token { + + T* item_pointer = nullptr; + + explicit Widget(T* pointer) noexcept + : item_pointer { pointer } { } + + explicit Widget(auto&&... args) noexcept + requires std::constructible_from + : item_pointer { new T { std::forward(args)... } } { } + + auto apply(auto& layout) const { layout.addWidget(item_pointer); } +}; + +// 传入一个方法用来辅助构造,在没有想要的接口时用这个吧 +template +struct Apply : Token { + Lambda lambda; + explicit Apply(Lambda lambda) noexcept + : lambda { lambda } { } + auto apply(auto& self) const noexcept -> void { + if constexpr (std::invocable) lambda(); + if constexpr (std::invocable) lambda(self); + } +}; + +template +concept trait = std::derived_from; + +CREEPER_DEFINE_CHECKER(trait); +} diff --git a/modern-qt/utility/wrapper/mutable-value.hh b/modern-qt/utility/wrapper/mutable-value.hh new file mode 100644 index 0000000..ebdf01f --- /dev/null +++ b/modern-qt/utility/wrapper/mutable-value.hh @@ -0,0 +1,153 @@ +#pragma once +#include "modern-qt/utility/wrapper/widget.hh" +#include +#include + +namespace creeper { + +template +struct MutableValue final { + T value; + + struct Functor { + virtual ~Functor() noexcept = default; + virtual void update(const T&) = 0; + }; + std::unordered_map> callbacks; + + struct Nothing { }; + std::shared_ptr alive = std::make_shared(); + + MutableValue(const MutableValue&) = delete; + MutableValue(MutableValue&&) = delete; + + template + explicit MutableValue(Args&&... args) noexcept + requires std::constructible_from + : value { std::forward(args)... } { } + + constexpr auto get() const noexcept -> T { return value; } + constexpr operator T() const noexcept { return get(); } + + template + auto set(U&& u) noexcept -> void + requires requires(T& t, U&& u) { t = std::forward(u); } + { + value = std::forward(u); + for (const auto& [_, f] : callbacks) + f->update(value); + } + template + auto set_silent(U&& u) noexcept -> void + requires requires(T& t, U&& u) { t = std::forward(u); } + { + value = std::forward(u); + } + template + auto operator=(U&& u) noexcept -> void + requires requires(T& t, U&& u) { t = std::forward(u); } + { + set(std::forward(u)); + } +}; + +template +struct MutableForward final : public P { + + MutableValue& mutable_value; + + explicit MutableForward(MutableValue& m) noexcept + requires std::constructible_from + : mutable_value { m } + , P { m.get() } { } + + template