添加creeper-qt最新依赖

This commit is contained in:
2025-11-25 15:59:47 +08:00
parent 0ec07218ab
commit fb1a30fc94
89 changed files with 8520 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
#include "animatable.hh"
using namespace creeper;
#include <qtimer.h>
using qwidget = QWidget;
using qtimer = QTimer;
struct Animatable::Impl {
qwidget& component;
qtimer scheduler;
std::vector<std::unique_ptr<ITransitionTask>> transition_tasks;
explicit Impl(auto& component, int hz = 90) noexcept
: component { component } {
scheduler.connect(&scheduler, &qtimer::timeout, [this] { update(); });
scheduler.setInterval(1'000 / hz);
}
auto set_frame_rate(int hz) noexcept -> void { scheduler.setInterval(1'000 / hz); }
auto push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void {
transition_tasks.push_back(std::move(task));
if (!scheduler.isActive()) scheduler.start();
}
auto update() noexcept -> void {
const auto [first, last] = std::ranges::remove_if(transition_tasks,
[](const std::unique_ptr<ITransitionTask>& task) { return !task->update(); });
component.update();
transition_tasks.erase(first, last);
if (transition_tasks.empty()) {
scheduler.stop();
}
}
};
Animatable::Animatable(QWidget& component) noexcept
: pimpl { std::make_unique<Impl>(component) } { }
Animatable::~Animatable() = default;
auto Animatable::set_frame_rate(int hz) noexcept -> void {
pimpl->set_frame_rate(hz); //
}
auto Animatable::push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void {
pimpl->push_transition_task(std::move(task));
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "creeper-qt/utility/wrapper/pimpl.hh"
#include <qwidget.h>
namespace creeper {
/// @note
/// Ends after the calculation is completed or the controller call ends
struct ITransitionTask {
virtual ~ITransitionTask() noexcept = default;
virtual auto update() noexcept -> bool = 0;
};
class Animatable {
CREEPER_PIMPL_DEFINITION(Animatable)
public:
explicit Animatable(QWidget& widget) noexcept;
auto set_frame_rate(int hz) noexcept -> void;
auto get_frame_rate() const noexcept -> int;
auto push_transition_task(std::unique_ptr<ITransitionTask> task) noexcept -> void;
};
}

View File

@@ -0,0 +1,95 @@
#pragma once
#include <eigen3/Eigen/Dense>
#include <qcolor.h>
#include <qrect.h>
namespace creeper::animate {
template <typename T>
constexpr auto zero() noexcept {
if constexpr (std::is_arithmetic_v<T>) {
return T { 0 };
} else if constexpr (requires { T::Zero(); }) {
return T::Zero();
} else {
static_assert(sizeof(T) == 0, "zero() not implemented for this type");
}
}
template <typename T>
constexpr auto magnitude(const T& error) noexcept {
if constexpr (std::is_arithmetic_v<T>) {
return std::abs(error);
} else if constexpr (requires { error.norm(); }) {
return std::abs(error.norm());
} else {
static_assert(sizeof(T) == 0, "magnitude() not implemented for this type");
}
}
template <typename T>
constexpr auto normalize(const T& error) noexcept {
if constexpr (std::is_arithmetic_v<T>) {
return error;
} else if constexpr (requires { error.norm(); }) {
return error.norm();
} else {
static_assert(sizeof(T) == 0, "magnitude() not implemented for this type");
}
}
template <typename T>
constexpr auto interpolate(const T& start, const T& end, double t) noexcept -> T {
const auto clamped_t = std::clamp(t, 0., 1.);
if constexpr (std::is_arithmetic_v<T>) {
return static_cast<T>(start + (end - start) * clamped_t);
} else if constexpr ( //
requires(const T& a, const T& b, const double f) {
{ a - b } -> std::convertible_to<T>;
{ a * f } -> std::convertible_to<T>;
{ a + b } -> std::convertible_to<T>;
}) {
return start + (end - start) * clamped_t;
} else {
static_assert(sizeof(T) == 0,
"interpolate() requires T to be an arithmetic type or define +, -, and scalar "
"multiplication.");
}
}
constexpr auto interpolate(const QRectF& start, const QRectF& end, double position) -> QRectF {
position = qBound(0.0, position, 1.0);
auto _1 = start.left() + (end.left() - start.left()) * position;
auto _2 = start.top() + (end.top() - start.top()) * position;
auto _3 = start.width() + (end.width() - start.width()) * position;
auto _4 = start.height() + (end.height() - start.height()) * position;
return { _1, _2, _3, _4 };
}
}
namespace creeper {
constexpr auto from_color(const QColor& color) -> Eigen::Vector4d {
return Eigen::Vector4d(color.red(), color.green(), color.blue(), color.alpha());
}
constexpr auto from_vector4(const Eigen::Vector4d& vector) -> QColor {
return QColor(vector[0], vector[1], vector[2], vector[3]);
}
constexpr auto extract_rect(const QRectF& rect, double w_weight, double h_weight) -> QRectF {
double rw, rh;
if (rect.width() * h_weight > rect.height() * w_weight) {
rh = rect.height();
rw = rh * w_weight / h_weight;
} else {
rw = rect.width();
rh = rw * h_weight / w_weight;
}
const auto center = rect.center();
return QRectF(center.x() - rw / 2, center.y() - rh / 2, rw, rh);
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
namespace creeper {
struct NormalAccessor {
auto get_value(this auto const& self) { return self.value; }
auto set_value(this auto& self, auto const& t) { self.value = t; }
auto get_target(this auto const& self) { return self.target; }
auto set_target(this auto& self, auto const& t) { self.target = t; }
};
}

View File

@@ -0,0 +1,75 @@
#pragma once
#include "creeper-qt/utility/animation/math.hh"
#include "creeper-qt/utility/animation/state/accessor.hh"
#include <chrono>
namespace creeper {
template <typename T>
struct LinearState : public NormalAccessor {
using ValueT = T;
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
T value = animate::zero<T>();
T target = animate::zero<T>();
struct {
double speed = 1.0;
double epsilon = 1e-2;
} config;
struct {
TimePoint last_timestamp;
} details;
auto set_target(T new_target) noexcept -> void {
target = new_target;
const auto current_time = Clock::now();
using namespace std::chrono_literals;
const auto threshold = 16ms;
const auto elapsed_time = current_time - details.last_timestamp;
if (elapsed_time > threshold) {
details.last_timestamp = current_time;
}
}
auto update() noexcept -> bool {
const auto now = Clock::now();
const auto duration = now - details.last_timestamp;
const auto dt = std::chrono::duration<double>(duration).count();
if (dt <= 0.0) {
details.last_timestamp = now;
return animate::magnitude(target - value) > config.epsilon;
}
const auto delta = target - value;
const auto dist = animate::magnitude(delta);
if (dist <= config.epsilon) {
value = target;
details.last_timestamp = now;
return false;
}
const auto direction = animate::normalize(delta);
const auto step = config.speed * dt;
if (step >= dist) {
value = target;
} else {
value += direction * step;
}
details.last_timestamp = now;
return true;
}
};
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include "creeper-qt/utility/animation/math.hh"
#include "creeper-qt/utility/animation/state/accessor.hh"
#include <chrono>
namespace creeper {
template <typename T>
struct PidState : public NormalAccessor {
using ValueT = T;
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
T value = animate::zero<T>();
T target = animate::zero<T>();
struct {
double kp = 1.0;
double ki = 0.0;
double kd = 0.1;
double epsilon = 1e-3;
} config;
struct {
T integral_error = animate::zero<T>();
T last_error = animate::zero<T>();
TimePoint last_timestamp;
} details;
auto set_target(T new_target) noexcept -> void {
target = new_target;
const auto current_time = Clock::now();
using namespace std::chrono_literals;
const auto threshold = 16ms;
const auto elapsed_time = current_time - details.last_timestamp;
if (elapsed_time > threshold) {
details.last_error = target - value;
details.last_timestamp = current_time;
}
}
auto update() noexcept -> bool {
const auto kp = config.kp;
const auto ki = config.ki;
const auto kd = config.kd;
const auto now = Clock::now();
const auto duration = now - details.last_timestamp;
const auto dt = std::chrono::duration<double>(duration).count();
if (dt <= 0.0) {
details.last_timestamp = now;
return animate::magnitude(target - value) > config.epsilon;
}
const auto current_error = target - value;
if (animate::magnitude(current_error) <= config.epsilon
&& animate::magnitude(details.last_error) <= config.epsilon) {
value = target;
details.integral_error = animate::zero<T>();
details.last_error = animate::zero<T>();
details.last_timestamp = now;
return false;
}
const auto proportional_term = kp * current_error;
details.integral_error += current_error * dt;
const auto integral_term = ki * details.integral_error;
const auto derivative_error = (current_error - details.last_error) / dt;
const auto derivative_term = kd * derivative_error;
const auto output = proportional_term + integral_term + derivative_term;
value += output * dt;
details.last_error = current_error;
details.last_timestamp = now;
return true;
}
};
}

View File

@@ -0,0 +1,72 @@
#pragma once
#include "creeper-qt/utility/animation/math.hh"
#include "creeper-qt/utility/animation/state/accessor.hh"
#include <chrono>
namespace creeper {
template <typename T>
struct SpringState : public NormalAccessor {
using ValueT = T;
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
T value;
T target;
T velocity = animate::zero<T>();
TimePoint last_timestamp = Clock::now();
struct {
double k = 1.0;
double d = 0.1;
double epsilon = 1e-1;
} config;
auto set_target(T new_target) noexcept -> void {
target = new_target;
const auto current_time = Clock::now();
using namespace std::chrono_literals;
const auto threshold = 16ms;
const auto elapsed_time = current_time - last_timestamp;
if (elapsed_time > threshold) {
const auto error = target - value;
velocity = animate::zero<T>();
last_timestamp = current_time;
}
}
auto update() noexcept -> bool {
const auto now = Clock::now();
const auto duration = now - last_timestamp;
const double dt = std::chrono::duration<double>(duration).count();
if (dt <= 0.0) {
last_timestamp = now;
return std::abs(animate::magnitude(target - value)) > config.epsilon;
}
const auto error = value - target;
const auto a_force = -config.k * error;
const auto a_damping = -config.d * velocity;
const auto a_total = a_force + a_damping;
velocity += a_total * dt;
value += velocity * dt;
last_timestamp = now;
const bool done =
animate::magnitude(error) < config.epsilon && std::abs(velocity) < config.epsilon;
if (done) velocity = animate::zero<T>();
return !done;
}
};
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include "animatable.hh"
namespace creeper {
template <class T>
concept transition_state_trait = requires(T& t) {
typename T::ValueT;
{ t.get_value() } -> std::same_as<typename T::ValueT>;
{ t.get_target() } -> std::same_as<typename T::ValueT>;
{ t.set_value(std::declval<typename T::ValueT>()) };
{ t.set_target(std::declval<typename T::ValueT>()) };
{ t.update() } -> std::same_as<bool>;
};
// Functor like lambda
template <transition_state_trait State>
struct TransitionTask : public ITransitionTask {
public:
explicit TransitionTask(std::shared_ptr<State> state, std::shared_ptr<bool> token) noexcept
: state { std::move(state) }
, running { std::move(token) } { }
~TransitionTask() override = default;
auto update() noexcept -> bool override {
return *running && state->update(); //
}
private:
std::shared_ptr<State> state;
std::shared_ptr<bool> running;
};
template <transition_state_trait State>
struct TransitionValue {
public:
using T = State::ValueT;
explicit TransitionValue(Animatable& animatable, std::shared_ptr<State> state) noexcept
: animatable { animatable }
, state { std::move(state) } { }
auto get_state() const noexcept -> const State& { return *state; }
auto get_value() const noexcept { return state->get_value(); }
auto get_target() const noexcept { return state->get_target(); }
operator T() const noexcept { return state->get_value(); }
auto transition_to(const T& to) noexcept -> void {
// Update target of state
state->set_target(to);
// Clear last transition task
if (running) {
*running = false;
}
running = std::make_shared<bool>(true);
// Push new transition task
auto task = std::make_unique<TransitionTask<State>>(state, running);
animatable.push_transition_task(std::move(task));
}
auto snap_to(T to) noexcept -> void {
state->set_value(std::move(to));
state->set_target(std::move(to));
if (running) *running = false;
}
private:
std::shared_ptr<State> state;
std::shared_ptr<bool> running;
Animatable& animatable;
};
template <transition_state_trait State>
inline auto make_transition(Animatable& core, std::shared_ptr<State> state) {
return std::make_unique<TransitionValue<State>>(core, state);
}
}

View File

@@ -0,0 +1,71 @@
#pragma once
#include "creeper-qt/utility/animation/state/accessor.hh"
#include "creeper-qt/utility/animation/transition.hh"
#include <QColor>
#include <QPainter>
#include <QPainterPath>
#include <QPointF>
#include <memory>
#include <vector>
namespace creeper {
struct WaterRippleState : public NormalAccessor {
using ValueT = double;
QPointF origin;
double value = 0.0;
double target = 0.0;
double speed = 1.0;
auto update() noexcept -> bool {
value += speed;
return value < target;
}
};
class WaterRippleRenderer {
public:
explicit WaterRippleRenderer(Animatable& core, double speed)
: animatable { core }
, speed { speed } { }
auto clicked(const QPointF& origin, double max_distance) noexcept -> void {
auto state = std::make_shared<WaterRippleState>();
state->origin = origin;
state->speed = speed;
state->target = max_distance;
auto ripple = make_transition(animatable, state);
ripple->transition_to(max_distance);
ripples.push_back(std::move(ripple));
}
auto renderer(const QPainterPath& clip_path, const QColor& water_color) noexcept {
return [&, this](QPainter& painter) {
std::erase_if(ripples, [&](const auto& ripple) {
const auto& state = ripple->get_state();
const auto opacity = 1.0 - state.value / state.target;
painter.setRenderHint(QPainter::Antialiasing);
painter.setClipPath(clip_path);
painter.setOpacity(opacity);
painter.setPen(Qt::NoPen);
painter.setBrush(water_color);
painter.drawEllipse(state.origin, state.value, state.value);
painter.setOpacity(1.0);
return state.value >= state.target;
});
};
}
private:
std::vector<std::unique_ptr<TransitionValue<WaterRippleState>>> ripples;
Animatable& animatable;
double speed;
};
}

View File

@@ -0,0 +1,63 @@
#pragma once
#include <qpixmap.h>
namespace creeper {
struct ContentScale {
public:
enum : uint8_t {
NONE,
FIT,
CROP,
FILL_WIDTH,
FILL_HEIGHT,
FILL_BOUNDS,
INSIDE,
} data;
operator decltype(data)() const noexcept { return data; }
auto transform(const QPixmap& pixmap, const QSize& size) const -> QPixmap {
if (pixmap.isNull()) return {};
auto image_size = QPointF(pixmap.width(), pixmap.height());
auto target_width = static_cast<double>(size.width());
auto target_height = static_cast<double>(size.height());
constexpr auto mode = Qt::SmoothTransformation;
switch (data) {
case ContentScale::NONE: {
return pixmap;
}
case ContentScale::FIT: {
image_size *= target_width / image_size.x();
if (image_size.y() > target_height) image_size *= target_height / image_size.y();
return pixmap.scaled(image_size.x(), image_size.y(), Qt::IgnoreAspectRatio, mode);
}
case ContentScale::CROP: {
image_size *= target_width / image_size.x();
if (image_size.y() < target_height) image_size *= target_height / image_size.y();
return pixmap.scaled(image_size.x(), image_size.y(), Qt::IgnoreAspectRatio, mode);
}
case ContentScale::INSIDE: {
if (image_size.x() > target_width) image_size *= target_width / image_size.x();
if (image_size.y() > target_height) image_size *= target_height / image_size.y();
return pixmap.scaled(image_size.x(), image_size.y(), Qt::IgnoreAspectRatio, mode);
}
case ContentScale::FILL_BOUNDS: {
return pixmap.scaled(size, Qt::IgnoreAspectRatio, mode);
}
case ContentScale::FILL_WIDTH:
return pixmap.scaled(target_width, image_size.y() * target_width / image_size.x(),
Qt::IgnoreAspectRatio, mode);
case ContentScale::FILL_HEIGHT:
return pixmap.scaled(image_size.x() * target_height / image_size.y(), target_height,
Qt::IgnoreAspectRatio, mode);
}
return {};
}
};
}

View File

@@ -0,0 +1,178 @@
#pragma once
#include <qfont.h>
#include <qfontdatabase.h>
namespace creeper {
namespace material {
namespace size {
constexpr auto _0 = int { 15 };
constexpr auto _1 = int { 18 };
constexpr auto _2 = int { 18 };
constexpr auto _3 = int { 24 };
constexpr auto _4 = int { 32 };
}
class FontLoader {
// static inline QString sharp_font_name = "";
// static inline QString round_font_name = "";
// static inline QString outline_font_name = "";
static inline QString material_font = "";
public:
static void load_font() {
int fontId = QFontDatabase::addApplicationFont(":/ttf/ttf/MaterialIcons-Regular.ttf");
material_font = get_font_family(fontId, "Material Icons");
}
private:
static QString get_font_family(int fontId, const QString& fallback) {
if (fontId == -1) {
return fallback;
}
QStringList families = QFontDatabase::applicationFontFamilies(fontId);
if (families.isEmpty()) {
return fallback;
}
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;
constexpr auto kRegularFontName = regular::font;
inline const auto kRegularExtraSmallFont = regular::font_0;
inline const auto kRegularSmallFont = regular::font_1;
inline const auto kRegularMediumFont = regular::font_2;
inline const auto kRegularLargeFont = regular::font_3;
inline const auto kRegularExtraLargeFont = regular::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";
constexpr auto kPets = "pets";
// 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";
constexpr auto kFileExport = "file_export";
// 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";
// sensor icon
constexpr auto kTouchSensor = "touch_app";
// setting page
constexpr auto kBorderColor = "border_color";
}
}
}

View File

@@ -0,0 +1,121 @@
#pragma once
#include <qnetworkaccessmanager.h>
#include <qnetworkreply.h>
#include <qpixmap.h>
namespace creeper {
namespace painter_resource {
template <typename T>
concept finished_callback_c = std::invocable<T> || std::invocable<T, QPixmap&>;
}
struct PainterResource : public QPixmap {
constexpr explicit PainterResource(std::string_view url) noexcept
: QPixmap {} {
const auto qurl = QUrl(QString::fromUtf8(url.data(), static_cast<int>(url.size())));
if (is_filesystem_url(url) || is_qt_resource_url(url)) {
QPixmap::load(qurl.path());
} else if (is_network_url(url)) {
download_resource_from_network(qurl, [](auto&) { });
} else {
qWarning() << "[PainterResource] Failed to recognize the type of url";
}
}
constexpr explicit PainterResource(std::string_view url, auto&& f) noexcept
requires painter_resource::finished_callback_c<decltype(f)>
{
const auto qurl = QUrl(QString::fromUtf8(url.data(), static_cast<int>(url.size())));
if (is_network_url(url)) {
download_resource_from_network(qurl, f);
} else {
qWarning() << "[PainterResource] Only network url can be used with callback";
}
}
~PainterResource() noexcept { *resource_exiting = false; }
template <typename T>
explicit PainterResource(T&& other) noexcept
requires std::convertible_to<T, QPixmap>
: QPixmap(std::forward<T>(other)) { }
template <typename T>
auto operator=(T&& other) noexcept -> PainterResource&
requires std::convertible_to<T, QPixmap>
{
QPixmap::operator=(std::forward<T>(other));
return *this;
}
auto is_loading() const noexcept -> bool { return is_loading_; }
auto is_error() const noexcept -> bool { return is_error_; }
auto add_finished_callback(std::invocable<PainterResource&> auto&& f) {
finished_callback_ = std::forward<decltype(f)>(f);
}
private:
std::optional<std::function<void(PainterResource&)>> finished_callback_;
bool is_loading_ = false;
bool is_error_ = false;
std::shared_ptr<bool> resource_exiting = std::make_shared<bool>(true);
auto download_resource_from_network(const QUrl& url, auto&& f) noexcept -> void
requires painter_resource::finished_callback_c<decltype(f)>
{
is_loading_ = true;
auto manager = new QNetworkAccessManager;
auto replay = manager->get(QNetworkRequest { url });
auto resource_exiting = this->resource_exiting;
QObject::connect(replay, &QNetworkReply::finished, [=, this] {
if (!*resource_exiting) {
qWarning() << "[PainterResource] Async task aborted: "
"Resource instance has been destroyed.";
return;
}
const auto error = replay->error();
const auto data = replay->readAll();
if (error != QNetworkReply::NoError) {
is_error_ = true;
qWarning() << "[PainterResource] Network error:" << replay->errorString();
} else if (data.isNull()) {
is_error_ = true;
} else {
is_error_ = false;
loadFromData(data);
}
is_loading_ = false;
manager->deleteLater();
using F = decltype(f);
if constexpr (std::invocable<F, PainterResource&>) std::invoke(f, *this);
if constexpr (std::invocable<F>) std::invoke(f);
if (finished_callback_) std::invoke(*finished_callback_, *this);
});
}
static constexpr auto starts_with(std::string_view s, std::string_view prefix) -> bool {
return s.substr(0, prefix.size()) == prefix;
}
static constexpr auto is_filesystem_url(std::string_view url) -> bool {
return !starts_with(url, "http://") && !starts_with(url, "https://")
&& !starts_with(url, "qrc:/") && !starts_with(url, ":/");
}
static constexpr auto is_qt_resource_url(std::string_view url) -> bool {
return starts_with(url, "qrc:/") || starts_with(url, ":/");
}
static constexpr auto is_network_url(std::string_view url) -> bool {
return starts_with(url, "http://") || starts_with(url, "https://");
}
};
}

View File

@@ -0,0 +1,99 @@
#pragma once
#include "creeper-qt/utility/wrapper/property.hh"
#include <qpainter.h>
namespace creeper::qt {
using painter = QPainter;
using point = QPointF;
using size = QSizeF;
using rect = QRectF;
using color = QColor;
using real = qreal;
using align = Qt::Alignment;
using string = QString;
using font = QFont;
using text_option = QTextOption;
using icon = QIcon;
}
namespace creeper::painter {
template <class T>
concept common_trait = requires(T t) {
{ auto { t.origin } } -> std::same_as<qt::point>;
{ auto { t.size } } -> std::same_as<qt::size>;
};
template <class T>
concept container_trait = requires(T t) {
{ auto { t.align } } -> std::same_as<qt::align>;
} && common_trait<T>;
template <class T>
concept shape_trait = requires(T t) {
{ auto { t.color_container } } -> std::same_as<qt::color>;
{ auto { t.color_outline } } -> std::same_as<qt::color>;
{ auto { t.thickness_outline } } -> std::same_as<qt::real>;
};
template <class T>
concept drawable_trait = common_trait<T> && std::invocable<T, qt::painter&>;
struct CommonProps {
qt::point origin = qt::point { 0, 0 };
qt::size size = qt::size { 0, 0 };
auto rect() const { return qt::rect { origin, size }; }
};
struct ContainerProps {
qt::size size = qt::size { 0, 0 };
qt::align align = qt::align {};
qt::point origin = qt::point { 0, 0 };
auto rect() const { return qt::rect { origin, size }; }
};
struct ShapeProps {
qt::color container_color = Qt::transparent;
qt::color outline_color = Qt::transparent;
qt::real outline_width = 0;
};
}
namespace creeper::painter::common::pro {
struct Token { };
using Size = DerivedProp<Token, qt::size, [](auto& self, auto const& v) { self.size = v; }>;
using Origin = DerivedProp<Token, qt::point, [](auto& self, auto const& v) { self.origin = v; }>;
using ContainerColor =
SetterProp<Token, qt::color, [](auto& self, auto const& v) { self.container_color = v; }>;
using OutlineColor =
SetterProp<Token, qt::color, [](auto& self, auto const& v) { self.outline_color = v; }>;
using OutlineWidth =
SetterProp<Token, qt::real, [](auto& self, auto const& v) { self.outline_width = v; }>;
struct Outline : Token {
qt::color color;
qt::real width;
Outline(const qt::color& color, qt::real width)
: color { color }
, width { width } { }
auto apply(auto& self) {
self.outline_color = color;
self.outline_width = width;
}
};
/// Alias
using Fill = ContainerColor;
/// Export
template <class T>
concept trait = std::derived_from<T, Token>;
CREEPER_DEFINE_CHECKER(trait);
}

View File

@@ -0,0 +1,343 @@
#pragma once
#include "creeper-qt/utility/painter/common.hh"
namespace creeper::painter {
template <class Impl, drawable_trait... Ts>
struct Container : public Impl {
std::tuple<std::decay_t<Ts>...> drawable;
constexpr explicit Container(const Impl& impl, Ts&&... drawable)
: Impl { impl }
, drawable { std::make_tuple(std::forward<Ts>(drawable)...) } { }
auto operator()(qt::painter& painter)
requires(std::invocable<Ts, qt::painter&> && ...)
{
render(painter);
}
auto render(qt::painter& painter) noexcept
requires(std::invocable<Ts, qt::painter&> && ...)
{
constexpr auto has_unique = requires { //
static_cast<Impl&>(*this).unique_render(painter, drawable);
};
if constexpr (has_unique) {
static_cast<Impl&>(*this).unique_render(painter, drawable);
} else {
Impl::make_layout(drawable);
auto f = [&](auto&... d) { (d(painter), ...); };
std::apply(std::move(f), drawable);
}
}
};
// ----------------------------------------------------------------------
// 布局实现基类
// ----------------------------------------------------------------------
struct MakeLayoutFunction {
template <drawable_trait... Ts>
auto make_layout(this auto& self, std::tuple<Ts...>& drawable) {
std::apply([&self](auto&... d) { ((self.make(d)), ...); }, drawable);
}
};
// ----------------------------------------------------------------------
// SurfaceImpl
// ----------------------------------------------------------------------
struct SurfaceImpl : public MakeLayoutFunction, ContainerProps {
constexpr explicit SurfaceImpl(const qt::size& size, const qt::point& origin = {})
: ContainerProps {
.size = size,
.origin = origin,
} { }
auto make(drawable_trait auto& drawable) {
const auto& container_origin = origin;
auto& drawable_origin = drawable.origin;
drawable_origin.setX(container_origin.x() + drawable_origin.x());
drawable_origin.setY(container_origin.y() + drawable_origin.y());
};
};
// ----------------------------------------------------------------------
// BufferImpl
// ----------------------------------------------------------------------
struct BufferImpl : public MakeLayoutFunction, ContainerProps {
mutable QPixmap buffer;
constexpr explicit BufferImpl(const qt::size& size, const qt::point& origin = { 0, 0 })
: ContainerProps {
.size = size,
.origin = origin,
} { }
auto make(drawable_trait auto& drawable) {
const auto& container_origin = origin;
drawable.origin.setX(container_origin.x() + drawable.origin.x());
drawable.origin.setY(container_origin.y() + drawable.origin.y());
};
template <drawable_trait... Ts>
auto unique_render(qt::painter& main_painter, std::tuple<Ts...>& drawable) noexcept {
make_layout(drawable);
if (buffer.size() != size || buffer.isNull()) {
buffer = QPixmap(size.width(), size.height());
buffer.fill(Qt::transparent);
}
buffer.fill(Qt::transparent);
auto buffer_painter = qt::painter { &buffer };
buffer_painter.translate(-origin.x(), -origin.y());
const auto f = [&](auto&... args) {
(
[&]() {
buffer_painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
args(buffer_painter);
}(),
...);
};
std::apply(std::move(f), drawable);
buffer_painter.end();
main_painter.drawPixmap(origin, buffer);
}
};
// ----------------------------------------------------------------------
// BoxImpl (居中对齐)
// ----------------------------------------------------------------------
struct BoxImpl : public MakeLayoutFunction, ContainerProps {
constexpr explicit BoxImpl(
const qt::size& size, const qt::align& align, const qt::point& origin = {})
: ContainerProps {
.size = size,
.align = align,
.origin = origin,
} { }
auto make(drawable_trait auto& drawable) {
const auto container_align = align;
const auto container_size = size;
const auto container_origin = origin;
auto& drawable_origin = drawable.origin;
auto& drawable_size = drawable.size;
const auto container_w = container_size.width();
const auto container_h = container_size.height();
if (container_align & Qt::AlignRight) {
drawable_origin.setX(container_origin.x() + container_w - drawable_size.width());
} else if (container_align & Qt::AlignHCenter) {
const auto dx = (container_w - drawable_size.width()) / 2;
drawable_origin.setX(container_origin.x() + dx);
} else {
drawable_origin.setX(container_origin.x());
}
if (container_align & Qt::AlignBottom) {
drawable_origin.setY(container_origin.y() + container_h - drawable_size.height());
} else if (container_align & Qt::AlignVCenter) {
const auto dy = (container_h - drawable_size.height()) / 2;
drawable_origin.setY(container_origin.y() + dy);
} else {
drawable_origin.setY(container_origin.y());
}
};
};
// ----------------------------------------------------------------------
// RowImpl (横向流式布局)
// ----------------------------------------------------------------------
struct RowImpl : public MakeLayoutFunction, ContainerProps {
const qt::align main_align;
constexpr explicit RowImpl(
const qt::size& size,
const qt::align& main_align = Qt::AlignLeft,
const qt::align& cross_align = Qt::AlignVCenter,
const qt::point& origin = {})
: ContainerProps {
.size = size,
.align = cross_align, // ContainerProps::align 存储非主轴对齐
.origin = origin,
}
, main_align(main_align) // 存储主轴对齐
{ }
mutable int current_x = 0;
template <drawable_trait... Ts>
auto make_layout(this auto& self, std::tuple<Ts...>& drawable) {
// 1. 计算主轴总尺寸 (Total Width)
int total_width = 0;
std::apply(
[&total_width](auto&... d) { ((total_width += d.size.width()), ...); }, drawable);
// 2. 计算主轴偏移 (Main Axis Offset)
int initial_x_offset = 0;
const int remaining_space = self.size.width() - total_width;
if (remaining_space > 0) {
if (self.main_align & Qt::AlignRight) {
initial_x_offset = remaining_space;
} else if (self.main_align & Qt::AlignHCenter) {
initial_x_offset = remaining_space / 2;
}
}
// 3. 设置起始 X 坐标
self.current_x = self.origin.x() + initial_x_offset;
// 4. 应用布局到每个元素
std::apply([&self](auto&... d) { ((self.make(d)), ...); }, drawable);
}
auto make(drawable_trait auto& drawable) {
const auto container_cross_align = align;
const auto container_size = size;
const auto container_origin = origin;
auto& drawable_origin = drawable.origin;
const auto drawable_h = drawable.size.height();
// 1. 主轴布局 (X 坐标累加)
drawable_origin.setX(current_x);
current_x += drawable.size.width();
// 2. 非主轴对齐 (垂直对齐)
if (container_cross_align & Qt::AlignBottom) {
drawable_origin.setY(container_origin.y() + container_size.height() - drawable_h);
} else if (container_cross_align & Qt::AlignVCenter) {
const auto dy = (container_size.height() - drawable_h) / 2;
drawable_origin.setY(container_origin.y() + dy);
} else { // 默认 AlignTop
drawable_origin.setY(container_origin.y());
}
};
};
// ----------------------------------------------------------------------
// ColImpl (垂直流式布局)
// ----------------------------------------------------------------------
struct ColImpl : public MakeLayoutFunction, ContainerProps {
const qt::align main_align;
constexpr explicit ColImpl(
const qt::size& size,
const qt::align& main_align = Qt::AlignTop,
const qt::align& cross_align = Qt::AlignHCenter,
const qt::point& origin = {})
: ContainerProps {
.size = size,
.align = cross_align,
.origin = origin,
}
, main_align(main_align) // 存储主轴对齐
{ }
mutable int current_y = 0;
template <drawable_trait... Ts>
auto make_layout(this auto& self, std::tuple<Ts...>& drawable) {
// 1. 计算主轴总尺寸 (Total Height)
int total_height = 0;
std::apply(
[&total_height](auto&... d) { ((total_height += d.size.height()), ...); }, drawable);
// 2. 计算主轴偏移 (Main Axis Offset)
int initial_y_offset = 0;
const int remaining_space = self.size.height() - total_height;
if (remaining_space > 0) {
if (self.main_align & Qt::AlignBottom) {
initial_y_offset = remaining_space;
} else if (self.main_align & Qt::AlignVCenter) {
initial_y_offset = remaining_space / 2;
}
}
// 3. 设置起始 Y 坐标
self.current_y = self.origin.y() + initial_y_offset;
// 4. 应用布局到每个元素
std::apply([&self](auto&... d) { ((self.make(d)), ...); }, drawable);
}
auto make(drawable_trait auto& drawable) {
const auto container_cross_align = align; // 非主轴对齐 (水平)
const auto container_size = size;
const auto container_origin = origin;
auto& drawable_origin = drawable.origin;
const auto drawable_w = drawable.size.width();
// 1. 主轴布局 (Y 坐标累加)
drawable_origin.setY(current_y);
current_y += drawable.size.height();
// 2. 非主轴对齐 (水平对齐)
if (container_cross_align & Qt::AlignRight) {
drawable_origin.setX(container_origin.x() + container_size.width() - drawable_w);
} else if (container_cross_align & Qt::AlignHCenter) {
const auto dx = (container_size.width() - drawable_w) / 2;
drawable_origin.setX(container_origin.x() + dx);
} else { // 默认 AlignLeft
drawable_origin.setX(container_origin.x());
}
};
};
// ----------------------------------------------------------------------
// 通用 Container 推导指引 (用于简化用户代码)
// ----------------------------------------------------------------------
template <typename... Ts>
Container(const SurfaceImpl& impl, Ts&&... args) -> Container<SurfaceImpl, Ts...>;
template <typename... Ts>
Container(const BufferImpl& impl, Ts&&... args) -> Container<BufferImpl, Ts...>;
template <typename... Ts>
Container(const BoxImpl& impl, Ts&&... args) -> Container<BoxImpl, Ts...>;
template <typename... Ts>
Container(const RowImpl& impl, Ts&&... args) -> Container<RowImpl, Ts...>;
template <typename... Ts>
Container(const ColImpl& impl, Ts&&... args) -> Container<ColImpl, Ts...>;
// ----------------------------------------------------------------------
// Paint 类型导出
// ----------------------------------------------------------------------
namespace Paint {
template <drawable_trait... Ts>
using Surface = Container<SurfaceImpl, Ts...>;
template <drawable_trait... Ts>
using Buffer = Container<BufferImpl, Ts...>;
template <drawable_trait... Ts>
using Box = Container<BoxImpl, Ts...>;
template <drawable_trait... Ts>
using Row = Container<RowImpl, Ts...>;
template <drawable_trait... Ts>
using Col = Container<ColImpl, Ts...>;
}
} // namespace creeper::painter

View File

@@ -0,0 +1,15 @@
#include "creeper-qt/utility/painter/helper.hh"
#include <qdebug.h>
namespace creeper::util {
constexpr auto enable_print_paint_event_count = bool { false };
auto print_paint_event_count() noexcept -> void {
if constexpr (enable_print_paint_event_count) {
static auto count = std::size_t { 0 };
qDebug() << "[PainterHelper] Paint Event:" << count++;
}
}
}

View File

@@ -0,0 +1,224 @@
#pragma once
#include <qpainter.h>
#include <qpainterpath.h>
namespace creeper::util {
auto print_paint_event_count() noexcept -> void;
/// @brief 隐藏冗杂的细节,解放命令式的绘图调用
class PainterHelper {
public:
using Renderer = std::function<void(QPainter&)>;
explicit PainterHelper(QPainter& painter)
: painter(painter) {
print_paint_event_count();
}
inline void done() { }
inline PainterHelper& apply(const Renderer& renderer) {
renderer(painter);
return *this;
}
inline PainterHelper& apply(const std::ranges::range auto& renderers)
requires std::same_as<std::ranges::range_value_t<decltype(renderers)>, Renderer>
{
for (const auto& renderer : renderers)
renderer(painter);
return *this;
}
public:
inline PainterHelper& set_brush(const QBrush& brush) {
painter.setBrush(brush);
return *this;
}
inline PainterHelper& set_pen(const QPen& pen) {
painter.setPen(pen);
return *this;
}
inline PainterHelper& set_opacity(double opacity) {
painter.setOpacity(opacity);
return *this;
}
inline PainterHelper& set_render_hint(QPainter::RenderHint hint, bool on = true) {
painter.setRenderHint(hint, on);
return *this;
}
inline PainterHelper& set_render_hints(QPainter::RenderHints hint, bool on = true) {
painter.setRenderHints(hint, on);
return *this;
}
inline PainterHelper& set_clip_path(
const QPainterPath& path, Qt::ClipOperation operation = Qt::ReplaceClip) {
painter.setClipPath(path, operation);
return *this;
}
public:
inline PainterHelper& point(const QColor& color, double radius, const QPointF& point) {
pen_only({ color, radius * 2 }).drawPoint(point);
return *this;
}
inline PainterHelper& ellipse(const QColor& background, const QColor& border_color,
double border_width, const QRectF& rect) {
brush_only({ background }).drawEllipse(rect);
const auto half = border_width / 2;
if (border_width != 0)
pen_only({ border_color, border_width })
.drawEllipse(rect.adjusted(half, half, -half, -half));
return *this;
}
inline PainterHelper& ellipse(const QColor& background, const QColor& border_color,
double border_width, const QPointF& origin, double radius_x, double radius_y) {
brush_only({ background }).drawEllipse(origin, radius_x, radius_y);
if (border_width != 0)
pen_only({ border_color, border_width })
.drawEllipse(origin, radius_x - border_width / 2, radius_y - border_width / 2);
return *this;
}
inline PainterHelper& rectangle(const QColor& background, const QColor& border_color,
double border_width, const QRectF& rect) {
brush_only({ background }).drawRect(rect);
if (border_width == 0) return *this;
const auto inliner_border_rectangle =
rect.adjusted(border_width / 2, border_width / 2, -border_width / 2, -border_width / 2);
pen_only({ border_color, border_width }).drawRect(inliner_border_rectangle);
return *this;
}
inline PainterHelper& rounded_rectangle(const QColor& background, const QColor& border_color,
double border_width, const QRectF& rect, double radius_x, double radius_y) {
brush_only({ background }).drawRoundedRect(rect, radius_x, radius_y);
if (border_width == 0) return *this;
const auto inliner_border_rectangle =
rect.adjusted(border_width / 2, border_width / 2, -border_width / 2, -border_width / 2);
pen_only({ border_color, border_width })
.drawRoundedRect(inliner_border_rectangle, std::max(radius_x - border_width / 2, 0.),
std::max(radius_y - border_width / 2, 0.));
return *this;
}
inline PainterHelper& rounded_rectangle(const QColor& background, const QColor& border_color,
double border_width, const QRectF& rect, double tl, double tr, double br, double bl) {
const auto path = make_rounded_rect_path(rect, tl, tr, br, bl);
brush_only({ background }).drawPath(path);
if (border_width == 0) return *this;
const auto inliner = [=](double r) { return std::max(r - border_width / 2, 0.); };
const auto inliner_border_rectangle =
rect.adjusted(border_width / 2, border_width / 2, -border_width / 2, -border_width / 2);
const auto inliner_path = make_rounded_rect_path(
inliner_border_rectangle, inliner(tl), inliner(tr), inliner(br), inliner(bl));
pen_only({ border_color, border_width }).drawPath(inliner_path);
return *this;
}
// Pen 是以路径为中心来绘制图片,有绘出 rect 导致画面被裁切的可能,由于是 path 类型,不好做限制
inline PainterHelper& path(const QColor& background, const QColor& border_color,
double border_width, const QPainterPath& path) {
brush_only({ background }).drawPath(path);
if (border_width != 0) pen_only({ border_color, border_width }).drawPath(path);
return *this;
}
inline PainterHelper& pixmap(const QPixmap& pixmap, const QRectF& dst, const QRectF& src) {
painter.drawPixmap(dst, pixmap, src);
return *this;
}
inline PainterHelper& simple_text(const QString& text, const QFont& font, const QColor& color,
const QRectF& rect, Qt::Alignment alignment) {
painter.setRenderHint(QPainter::TextAntialiasing);
painter.setFont(font);
painter.setBrush(Qt::NoBrush);
painter.setPen({ color });
painter.drawText(rect, alignment, text);
return *this;
}
private:
QPainter& painter;
QPainter& pen_only(const QPen& pen) {
painter.setBrush(Qt::NoBrush);
painter.setPen(pen);
return painter;
}
QPainter& brush_only(const QBrush& brush) {
painter.setBrush(brush);
painter.setPen(Qt::NoPen);
return painter;
}
static auto make_rounded_rect_path(
const QRectF& rect, qreal tl, qreal tr, qreal br, qreal bl) noexcept -> QPainterPath {
auto path = QPainterPath {};
const auto half_width = rect.width() / 2.0;
const auto half_height = rect.height() / 2.0;
const auto max_radius = std::min(half_width, half_height);
const auto clamp_radius = [&](qreal r) {
return r < 0 ? max_radius : std::min(r, max_radius);
};
tl = clamp_radius(tl);
tr = clamp_radius(tr);
br = clamp_radius(br);
bl = clamp_radius(bl);
path.moveTo(rect.topLeft() + QPointF(tl, 0));
path.lineTo(rect.topRight() - QPointF(tr, 0));
path.arcTo(
QRectF(rect.topRight().x() - 2 * tr, rect.topRight().y(), 2 * tr, 2 * tr), 90, -90);
path.lineTo(rect.bottomRight() - QPointF(0, br));
path.arcTo(QRectF(rect.bottomRight().x() - 2 * br, rect.bottomRight().y() - 2 * br, 2 * br,
2 * br),
0, -90);
path.lineTo(rect.bottomLeft() + QPointF(bl, 0));
path.arcTo(QRectF(rect.bottomLeft().x(), rect.bottomLeft().y() - 2 * bl, 2 * bl, 2 * bl),
270, -90);
path.lineTo(rect.topLeft() + QPointF(0, tl));
path.arcTo(QRectF(rect.topLeft().x(), rect.topLeft().y(), 2 * tl, 2 * tl), 180, -90);
path.closeSubpath();
return path;
}
};
}

View File

@@ -0,0 +1,257 @@
#pragma once
#include "creeper-qt/utility/painter/common.hh"
#include <qicon.h>
#include <qpainterpath.h>
namespace creeper::painter::internal {
struct EraseRectangle : public CommonProps {
auto operator()(qt::painter& painter) const noexcept {
painter.save();
painter.setCompositionMode(QPainter::CompositionMode_DestinationOut);
painter.setBrush(Qt::black);
painter.setPen(Qt::NoPen);
painter.drawRect(rect());
painter.restore();
}
};
struct Rectangle : public CommonProps, ShapeProps {
auto operator()(qt::painter& painter) const noexcept {
painter.save();
const auto rectangle = qt::rect { origin, size };
painter.setBrush(container_color);
painter.setPen(Qt::NoPen);
painter.drawRect(rectangle);
if (outline_width > 0) {
const auto thickness = outline_width / 2;
const auto inliner = rectangle.adjusted(thickness, thickness, -thickness, -thickness);
painter.setPen({ outline_color, outline_width });
painter.setBrush(Qt::NoBrush);
painter.drawRect(inliner);
}
painter.restore();
}
};
struct RoundedRectangle : public CommonProps, ShapeProps {
double radius_tl = 0;
double radius_tr = 0;
double radius_bl = 0;
double radius_br = 0;
auto set_radiuses(double r) {
radius_tl = r;
radius_tr = r;
radius_bl = r;
radius_br = r;
}
auto operator()(qt::painter& painter) const noexcept {
painter.save();
painter.setRenderHint(QPainter::Antialiasing);
const auto rect = qt::rect { origin, size };
const auto outline_shape =
make_rounded_rect_path(rect, radius_tl, radius_tr, radius_br, radius_bl);
painter.setPen(Qt::NoPen);
painter.setBrush(container_color);
painter.drawPath(outline_shape);
if (outline_width > 0) {
const auto thickness = outline_width;
const auto inliner_f = [=](double r) { return std::max(r - thickness / 2, 0.); };
const auto inliner_rect =
rect.adjusted(thickness / 2, thickness / 2, -thickness / 2, -thickness / 2);
const auto inliner_shape = make_rounded_rect_path(inliner_rect, inliner_f(radius_tl),
inliner_f(radius_tr), inliner_f(radius_br), inliner_f(radius_bl));
painter.setBrush(Qt::NoBrush);
painter.setPen({ outline_color, outline_width });
painter.drawPath(inliner_shape);
}
painter.restore();
}
static constexpr auto make_rounded_rect_path(
const qt::rect& rect, qreal tl, qreal tr, qreal br, qreal bl) noexcept -> QPainterPath {
auto path = QPainterPath {};
const auto max_radius = std::min(rect.width(), rect.height()) / 2.0;
const auto clamp = [&](qreal r) -> qreal {
return r < 0 ? max_radius : std::min(r, max_radius);
};
tl = clamp(tl);
tr = clamp(tr);
br = clamp(br);
bl = clamp(bl);
const auto Arc = [](qreal x, qreal y, qreal r,
int start_angle) -> std::tuple<qt::rect, int, int> {
return { qt::rect(x, y, 2 * r, 2 * r), start_angle, -90 };
};
path.moveTo(rect.topLeft() + qt::point(tl, 0));
path.lineTo(rect.topRight() - qt::point(tr, 0));
const auto [tr_rect, tr_start, tr_span] =
Arc(rect.topRight().x() - 2 * tr, rect.topRight().y(), tr, 90);
path.arcTo(tr_rect, tr_start, tr_span);
path.lineTo(rect.bottomRight() - qt::point(0, br));
const auto [br_rect, br_start, br_span] =
Arc(rect.bottomRight().x() - 2 * br, rect.bottomRight().y() - 2 * br, br, 0);
path.arcTo(br_rect, br_start, br_span);
path.lineTo(rect.bottomLeft() + qt::point(bl, 0));
const auto [bl_rect, bl_start, bl_span] =
Arc(rect.bottomLeft().x(), rect.bottomLeft().y() - 2 * bl, bl, 270);
path.arcTo(bl_rect, bl_start, bl_span);
path.lineTo(rect.topLeft() + qt::point(0, tl));
const auto [tl_rect, tl_start, tl_span] =
Arc(rect.topLeft().x(), rect.topLeft().y(), tl, 180);
path.arcTo(tl_rect, tl_start, tl_span);
path.closeSubpath();
return path;
}
};
struct Text : CommonProps {
qt::string text;
qt::font font;
qt::color color = Qt::black;
qt::text_option text_option;
qt::real scale = 1.;
auto operator()(qt::painter& painter) const noexcept {
painter.save();
painter.scale(scale, scale);
const auto origin_rect = rect();
const auto offset_x = origin_rect.x() * (1.0 - scale);
const auto center_y = origin_rect.y() + origin_rect.height() / 2.0;
const auto offset_y = center_y * (1.0 - scale);
painter.translate(offset_x, offset_y);
painter.setBrush(Qt::NoBrush);
painter.setPen(color);
painter.setFont(font);
const auto scaled_rect = qt::rect {
origin_rect.x() / scale,
origin_rect.y() / scale,
origin_rect.width() / scale,
origin_rect.height() / scale,
};
painter.drawText(scaled_rect, text, text_option);
painter.restore();
}
};
struct Icon : CommonProps {
using IconSource = qt::icon;
using FontSource = std::tuple<qt::string, qt::string>;
using MutiSource = std::variant<FontSource, IconSource>;
MutiSource source;
qt::color color = Qt::black;
auto operator()(qt::painter& painter) const noexcept {
if (size.isEmpty()) return;
painter.save();
const auto rect = qt::rect { origin, size };
std::visit(
[&](auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, FontSource>) {
const auto& [font_family, code] = value;
auto font = qt::font { font_family };
font.setPointSizeF(std::min(size.height(), size.width()));
auto option = qt::text_option {};
option.setAlignment(Qt::AlignCenter);
painter.setFont(font);
painter.setPen(color);
painter.setBrush(Qt::NoBrush);
painter.drawText(rect, code, option);
} else if constexpr (std::is_same_v<T, IconSource>) {
const auto& icon_source = value;
icon_source.paint(&painter, rect.toRect());
}
},
source);
painter.restore();
}
};
}
namespace creeper::painter {
/// Export Rounded Rectangle
using RadiusTL = SetterProp<common::pro::Token, double,
[](auto& self, auto radius) { self.radius_tl = radius; }>;
using RadiusTR = SetterProp<common::pro::Token, double,
[](auto& self, auto radius) { self.radius_tr = radius; }>;
using RadiusBL = SetterProp<common::pro::Token, double,
[](auto& self, auto radius) { self.radius_bl = radius; }>;
using RadiusBR = SetterProp<common::pro::Token, double,
[](auto& self, auto radius) { self.radius_br = radius; }>;
using Radiuses = SetterProp<common::pro::Token, double,
[](auto& self, auto radius) { self.set_radiuses(radius); }>;
/// Export Text
using Text = DerivedProp<common::pro::Token, qt::string,
[](auto& self, const auto& text) { self.text = text; }>;
using Font = DerivedProp<common::pro::Token, qt::font,
[](auto& self, const auto& font) { self.font = font; }>;
using Color = DerivedProp<common::pro::Token, qt::color,
[](auto& self, const auto& color) { self.color = color; }>;
using Scale = SetterProp<common::pro::Token, qt::real,
[](auto& self, const auto& scale) { self.scale = scale; }>;
using TextOption = DerivedProp<common::pro::Token, qt::text_option,
[](auto& self, const auto& option) { self.text_option = option; }>;
/// Export Icon
struct Icon : common::pro::Token {
using T = internal::Icon;
T::MutiSource source;
constexpr explicit Icon(const qt::string& font, const qt::string& code) noexcept
: source { T::FontSource { font, code } } { }
constexpr explicit Icon(const qt::icon& icon) noexcept
: source { T::IconSource { icon } } { }
auto apply(auto& self) const noexcept { self.source = source; }
};
namespace Paint {
using EraseRectangle = Declarative<internal::EraseRectangle, CheckerOr<common::pro::checker>>;
using Rectangle = Declarative<internal::Rectangle, CheckerOr<common::pro::checker>>;
using RoundedRectangle =
Declarative<internal::RoundedRectangle, CheckerOr<common::pro::checker>>;
using Text = Declarative<internal::Text, CheckerOr<common::pro::checker>>;
using Icon = Declarative<internal::Icon, CheckerOr<common::pro::checker>>;
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <QtGlobal>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QEnterEvent>
#else
#include <QEvent>
#endif
namespace creeper::qt {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
using EnterEvent = QEnterEvent;
#else
using EnterEvent = QEvent;
#endif
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <QtGlobal>
namespace creeper::qt {
inline auto margin_setter = [](auto& self, const auto& margin) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
self.setContentsMargins(margin, margin, margin, margin);
#else
self.setMargin(margin);
#endif
};
}

View File

@@ -0,0 +1,54 @@
#include "round-angle.hh"
#include <eigen3/Eigen/Dense>
using namespace creeper;
using Eigen::Vector2d, std::numbers::pi;
static inline auto impl_round_angle_solution(
RoundAngleSolution& solution, Vector2d e0, Vector2d e1, Vector2d e2, double radius) -> void {
// solve the arc origin
const auto v1 = Vector2d { e1 - e0 };
const auto v2 = Vector2d { e2 - e0 };
const auto dot = v1.x() * v2.x() + v1.y() * v2.y();
const auto det = v1.x() * v2.y() - v1.y() * v2.x();
const auto angle = std::abs(std::atan2(det, dot));
const auto width = radius / std::tan(angle / 2);
const auto point_begin = Vector2d { e0 + width * v1.normalized() };
const auto point_end = Vector2d { e0 + width * v2.normalized() };
const auto origin = Vector2d { point_begin + radius * v1.unitOrthogonal() };
solution.start = QPointF { point_begin.x(), point_begin.y() };
solution.end = QPointF { point_end.x(), point_end.y() };
// solve the rect corners
const auto v3 = Vector2d { e0 - origin }.normalized();
const auto v4 = Vector2d { v3.unitOrthogonal() };
const Vector2d corner0 = origin + Vector2d::UnitX() * radius + Vector2d::UnitY() * radius;
const Vector2d corner1 = origin - Vector2d::UnitX() * radius - Vector2d::UnitY() * radius;
solution.rect = QRectF { QPointF(corner1.x(), corner1.y()), QPointF(corner0.x(), corner0.y()) };
// solve the arc angle
// 角度计算时注意Qt的系Y的正方向向下但角度又是从X正方向逆时针开始计算,可谓混乱
const auto angle_begin_vector = Vector2d { point_begin - origin };
const auto angle_end_vector = Vector2d { point_end - origin };
const auto angle_end = std::atan2(-angle_end_vector.y(), angle_end_vector.x());
solution.angle_begin = std::atan2(-angle_begin_vector.y(), angle_begin_vector.x());
solution.angle_length = angle_end - solution.angle_begin;
if (solution.angle_length < -pi) solution.angle_length = 2 * pi + solution.angle_length;
solution.angle_begin = solution.angle_begin * 180 / pi;
solution.angle_length = solution.angle_length * 180 / pi;
}
RoundAngleSolution::RoundAngleSolution(QPointF e0, QPointF e1, QPointF e2, double radius) noexcept {
impl_round_angle_solution(
*this, Eigen::Vector2d { e0.x(), e0.y() }, { e1.x(), e1.y() }, { e2.x(), e2.y() }, radius);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <qpoint.h>
#include <qrect.h>
namespace creeper {
struct RoundAngleSolution {
/// @brief 给定原点和端点,按逆时针方向计算圆弧
/// @note 圆弧注意按照逆时针算
/// @param e0 两切线交点
/// @param e1 圆弧起始点切线
/// @param e2 圆弧终点切线
/// @param radius 半径
RoundAngleSolution(QPointF e0, QPointF e1, QPointF e2, double radius) noexcept;
QRectF rect;
QPointF start;
QPointF end;
double angle_begin;
double angle_length;
};
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include <qcolor.h>
#include <qfont.h>
namespace creeper::theme {
enum class ColorMode { LIGHT, DARK };
struct ColorScheme {
QColor primary;
QColor on_primary;
QColor primary_container;
QColor on_primary_container;
QColor secondary;
QColor on_secondary;
QColor secondary_container;
QColor on_secondary_container;
QColor tertiary;
QColor on_tertiary;
QColor tertiary_container;
QColor on_tertiary_container;
QColor error;
QColor on_error;
QColor error_container;
QColor on_error_container;
QColor background;
QColor on_background;
QColor surface;
QColor on_surface;
QColor surface_variant;
QColor on_surface_variant;
QColor outline;
QColor outline_variant;
QColor shadow;
QColor scrim;
QColor inverse_surface;
QColor inverse_on_surface;
QColor inverse_primary;
QColor surface_container_highest;
QColor surface_container_high;
QColor surface_container;
QColor surface_container_low;
QColor surface_container_lowest;
};
struct Typography {
QFont body;
QFont title;
QFont button;
};
}

View File

@@ -0,0 +1,101 @@
#pragma once
#include "creeper-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,
};
}

View File

@@ -0,0 +1,103 @@
#pragma once
#include "creeper-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,
};
}

View File

@@ -0,0 +1,107 @@
#pragma once
#include "creeper-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,
};
}

View File

@@ -0,0 +1,72 @@
#include "creeper-qt/utility/theme/theme.hh"
using namespace creeper::theme;
using Handler = ThemeManager::Handler;
struct ThemeManager::Impl {
using Key = const QObject*;
std::unordered_map<Key, Handler> handlers;
std::vector<Handler> begin_callbacks;
std::vector<Handler> final_callbacks;
ThemePack theme_pack;
ColorMode color_mode;
auto apply_theme(const ThemeManager& manager) const {
for (auto const& callback : begin_callbacks)
callback(manager);
for (auto& [_, callback] : handlers)
callback(manager);
for (auto const& callback : final_callbacks)
callback(manager);
}
auto append_handler(Key key, const Handler& handler) {
handlers[key] = handler;
QObject::connect(key, &QObject::destroyed, [this, key] { remove_handler(key); });
}
void remove_handler(Key key) { handlers.erase(key); }
};
ThemeManager::ThemeManager()
: pimpl(std::make_unique<Impl>()) { }
ThemeManager::ThemeManager(const ThemePack& pack, ColorMode mode)
: pimpl(std::make_unique<Impl>()) {
pimpl->theme_pack = pack;
pimpl->color_mode = mode;
}
ThemeManager::~ThemeManager() = default;
void ThemeManager::apply_theme() const { pimpl->apply_theme(*this); }
void ThemeManager::append_handler(const QObject* key, const Handler& handler) {
pimpl->append_handler(key, handler);
}
auto ThemeManager::append_begin_callback(const Handler& callback) noexcept -> void {
pimpl->begin_callbacks.push_back(callback);
}
auto ThemeManager::append_final_callback(const Handler& callback) noexcept -> void {
pimpl->final_callbacks.push_back(callback);
}
void ThemeManager::remove_handler(const QObject* key) { pimpl->remove_handler(key); }
void ThemeManager::set_theme_pack(const ThemePack& pack) { pimpl->theme_pack = pack; }
void ThemeManager::set_color_mode(const ColorMode& mode) { pimpl->color_mode = mode; }
void ThemeManager::toggle_color_mode() {
pimpl->color_mode = (pimpl->color_mode == ColorMode::LIGHT) //
? ColorMode::DARK
: ColorMode::LIGHT;
}
ThemePack ThemeManager::theme_pack() const { return pimpl->theme_pack; }
ColorMode ThemeManager::color_mode() const { return pimpl->color_mode; }
ColorScheme ThemeManager::color_scheme() const {
return pimpl->theme_pack.color_scheme(pimpl->color_mode);
}

View File

@@ -0,0 +1,112 @@
#pragma once
#include <qwidget.h>
#include "creeper-qt/utility/theme/color-scheme.hh"
#include "creeper-qt/utility/wrapper/common.hh"
#include "creeper-qt/utility/wrapper/pimpl.hh"
#include "creeper-qt/utility/wrapper/property.hh"
namespace creeper::theme {
class ThemeManager;
template <class T>
concept color_scheme_setter_trait = requires(T t) {
{ t.set_color_scheme(ColorScheme {}) };
};
template <class T>
concept theme_manager_loader_trait =
requires(T t, ThemeManager& manager) { t.load_theme_manager(manager); };
struct ThemePack {
ColorScheme light, dark;
auto color_scheme(this auto&& self, ColorMode mode) noexcept {
return (mode == ColorMode::LIGHT) ? self.light : self.dark;
}
};
class ThemeManager {
CREEPER_PIMPL_DEFINITION(ThemeManager)
public:
explicit ThemeManager(const ThemePack& pack, ColorMode mode = ColorMode::LIGHT);
void apply_theme() const;
using Handler = std::function<void(const ThemeManager&)>;
/// Registers a theme change callback for the specified widget.
///
/// When ThemeManager::apply_theme() is called, the registered handler will be executed.
///
/// Args:
/// key: Pointer to the widget. Serves as the key in the handler map.
/// handler: The callback function to register.
///
/// Note:
/// When the widget is destroyed, ThemeManager::remove_handler() will be called automatically
/// to remove the associated handler.
void append_handler(const QObject* key, const Handler& handler);
auto append_handler(color_scheme_setter_trait auto& widget) { append_handler(&widget, widget); }
auto append_handler(const QObject* key, color_scheme_setter_trait auto& widget) {
const auto handler = [&widget](const ThemeManager& manager) {
const auto color_mode = manager.color_mode();
const auto theme_pack = manager.theme_pack();
widget.set_color_scheme(theme_pack.color_scheme(color_mode));
};
append_handler(key, std::move(handler));
}
auto append_begin_callback(const Handler&) noexcept -> void;
auto append_final_callback(const Handler&) noexcept -> void;
void remove_handler(const QObject* key);
void set_theme_pack(const ThemePack& pack);
void set_color_mode(const ColorMode& mode);
void toggle_color_mode();
ThemePack theme_pack() const;
ColorMode color_mode() const;
ColorScheme color_scheme() const;
};
}
namespace creeper::theme::pro {
using Token = common::Token<ThemeManager>;
struct ColorScheme : public theme::ColorScheme, Token {
using theme::ColorScheme::ColorScheme;
explicit ColorScheme(const theme::ColorScheme& p)
: theme::ColorScheme(p) { }
auto apply(color_scheme_setter_trait auto& self) const noexcept -> void {
self.set_color_scheme(*this);
}
};
struct ThemeManager : Token {
theme::ThemeManager& manager;
explicit ThemeManager(theme::ThemeManager& p)
: manager(p) { }
auto apply(theme_manager_loader_trait auto& self) const noexcept -> void {
self.load_theme_manager(manager);
}
};
template <typename T>
concept trait = std::derived_from<T, Token>;
CREEPER_DEFINE_CHECKER(trait);
}
namespace creeper {
using ColorMode = theme::ColorMode;
using ColorScheme = theme::ColorScheme;
using ThemePack = theme::ThemePack;
using ThemeManager = theme::ThemeManager;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <concepts>
#include <qwidget.h>
#include <utility>
namespace creeper {
template <class T>
concept widget_trait = std::convertible_to<T*, QWidget*>;
template <class T>
concept widget_pointer_trait = std::convertible_to<T, QWidget*>;
template <class T>
concept layout_trait = std::convertible_to<T*, QLayout*>;
template <class T>
concept layout_pointer_trait = std::convertible_to<T, QLayout*>;
template <class T>
concept item_trait = widget_trait<T> || layout_trait<T>;
template <class T>
concept linear_trait = requires(T t) {
{ t.addWidget(std::declval<QWidget*>(), int {}, Qt::AlignCenter) };
{ t.addLayout(std::declval<QLayout*>(), int {}) };
};
template <class T>
concept stacked_trait = requires(T t) {
{t.addWidget(std::declval<QWidget*>())};
{t.insertWidget(int {}, std::declval<QWidget*>())};
};
template <class T>
concept area_trait = requires(T t) {
{ t.setWidget(std::declval<QWidget*>()) };
{ t.setLayout(std::declval<QLayout*>()) };
};
template <class T>
concept selectable_trait = requires(T t) {
{ std::as_const(t).selected() } -> std::convertible_to<bool>;
{ t.set_selected(bool {}) };
};
}

View File

@@ -0,0 +1,177 @@
#pragma once
#include "property.hh"
#include <qwidget.h>
namespace creeper::common {
template <typename Instance>
struct Token {
void apply(auto& self) const {
const auto self_name = typeid(self).name();
const auto prop_name = typeid(this).name();
// qDebug() << "Unimplemented" << prop_name << "is called by" << self_name;
}
};
namespace pro {
// 设置组件透明度
template <class Token>
using Opacity = SetterProp<Token, double, [](auto& self, double v) { self.set_opacity(v); }>;
// 设置圆角NXNY
template <class Token>
using RadiusNxNy =
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_nx_ny(v); }>;
// 设置圆角PXPY
template <class Token>
using RadiusPxPy =
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_px_py(v); }>;
// 设置圆角NXPY
template <class Token>
using RadiusNxPy =
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_nx_py(v); }>;
// 设置圆角PXNY
template <class Token>
using RadiusPxNy =
SetterProp<Token, double, [](auto& self, double v) { self.set_radius_px_ny(v); }>;
// 设置圆角X方向
template <class Token>
using RadiusX = SetterProp<Token, double, [](auto& self, double v) { self.set_radius_x(v); }>;
// 设置圆角Y方向
template <class Token>
using RadiusY = SetterProp<Token, double, [](auto& self, double v) { self.set_radius_y(v); }>;
// 设置通用圆角
template <class Token>
using Radius = SetterProp<Token, double, [](auto& self, double v) { self.set_radius(v); }>;
// 通用边界宽度
template <class Token>
using BorderWidth =
SetterProp<Token, double, [](auto& self, double v) { self.set_border_width(v); }>;
// 通用边界颜色
template <class Token>
using BorderColor =
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_border_color(v); }>;
// 通用文字颜色
template <class Token>
using TextColor =
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_text_color(v); }>;
// 通用背景颜色
template <class Token>
using Background =
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_background(v); }>;
// 通用水波纹颜色
template <class Token>
using WaterColor =
SetterProp<Token, QColor, [](auto& self, const QColor& v) { self.set_water_color(v); }>;
// 通用禁止属性
template <class Token>
using Disabled = SetterProp<Token, bool, [](auto& self, bool v) { self.set_disabled(v); }>;
// 通用 Checked 属性
template <class Token>
using Checked = SetterProp<Token, bool, [](auto& self, bool v) { self.set_checked(v); }>;
// 通用文本属性
template <class Token, auto setter>
struct String : public QString, Token {
using QString::QString;
explicit String(const QString& text) noexcept
: QString { text } { }
explicit String(const std::string& text) noexcept
: QString { QString::fromStdString(text) } { }
auto operator=(const QString& text) noexcept {
QString::operator=(text);
return *this;
}
auto operator=(QString&& text) noexcept {
QString::operator=(std::move(text));
return *this;
}
void apply(auto& self) const
requires requires { setter(self, *this); }
{
setter(self, *this);
}
};
template <class Token>
using Text = String<Token, [](auto& self, const auto& string) { self.setText(string); }>;
// 通用指针绑定
template <class Token, class Final>
struct Bind : Token {
Final*& widget;
explicit Bind(Final*& p)
: widget(p) { };
void apply(Final& self) const { widget = &self; }
};
// 通用点击事件
template <typename Callback, class Token>
struct Clickable : Token {
Callback callback;
explicit Clickable(Callback callback) noexcept
: callback { std::move(callback) } { }
auto apply(auto& self) const noexcept -> void
requires std::invocable<Callback, decltype(self)> || std::invocable<Callback>
{
using widget_t = std::remove_cvref_t<decltype(self)>;
QObject::connect(&self, &widget_t::clicked, [function = callback, &self] {
if constexpr (std::invocable<Callback, decltype(self)>) function(self);
if constexpr (std::invocable<Callback>) function();
});
}
};
// 自定义信号回调注册
namespace internal {
template <typename T>
struct FunctionArgs;
template <class C, class R, class... Args>
struct FunctionArgs<auto (C::*)(Args...)->R> {
using type = std::tuple<Args...>;
};
template <class C, class R, class... Args>
struct FunctionArgs<auto (C::*)(Args...) const->R> {
using type = std::tuple<Args...>;
};
template <typename F, typename Tuple>
concept tuple_invocable_trait = requires(F&& f, Tuple&& t) {
std::apply(std::forward<F>(f), std::forward<Tuple>(t)); //
};
}
template <typename F, class Token, auto signal>
struct SignalInjection : Token {
F f;
using SignalArgs = typename internal::FunctionArgs<decltype(signal)>::type;
explicit SignalInjection(F f) noexcept
requires internal::tuple_invocable_trait<F, SignalArgs>
: f { std::forward<decltype(f)>(f) } { }
auto apply(auto& self) const noexcept { QObject::connect(&self, signal, f); }
};
}
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include "creeper-qt/utility/qt_wrapper/margin-setter.hh"
#include "creeper-qt/utility/trait/widget.hh"
#include "creeper-qt/utility/wrapper/common.hh"
namespace creeper::layout::pro {
struct Layout { };
using Token = common::Token<Layout>;
using ContentsMargin = SetterProp<Token, QMargins,
[](auto& self, const auto& margins) { self.setContentsMargins(margins); }>;
using Alignment = SetterProp<Token, Qt::Alignment,
[](auto& self, const auto& alignment) { self.setAlignment(alignment); }>;
using Spacing =
SetterProp<Token, int, [](auto& self, const auto& spacing) { self.setSpacing(spacing); }>;
using Margin = SetterProp<Token, int, qt::margin_setter>;
template <widget_trait T>
struct Widget : Token {
T* item_pointer = nullptr;
explicit Widget(T* pointer) noexcept
: item_pointer { pointer } { }
explicit Widget(auto&&... args) noexcept
requires std::constructible_from<T, decltype(args)...>
: item_pointer { new T { std::forward<decltype(args)>(args)... } } { }
auto apply(auto& layout) const { layout.addWidget(item_pointer); }
};
// 传入一个方法用来辅助构造,在没有想要的接口时用这个吧
template <typename Lambda>
struct Apply : Token {
Lambda lambda;
explicit Apply(Lambda lambda) noexcept
: lambda { lambda } { }
auto apply(auto& self) const noexcept -> void {
if constexpr (std::invocable<Lambda>) lambda();
if constexpr (std::invocable<Lambda, decltype(self)>) lambda(self);
}
};
template <class T>
concept trait = std::derived_from<T, Token>;
CREEPER_DEFINE_CHECKER(trait);
}

View File

@@ -0,0 +1,153 @@
#pragma once
#include "creeper-qt/utility/wrapper/widget.hh"
#include <qobject.h>
#include <unordered_map>
namespace creeper {
template <typename T>
struct MutableValue final {
T value;
struct Functor {
virtual ~Functor() noexcept = default;
virtual void update(const T&) = 0;
};
std::unordered_map<QObject*, std::unique_ptr<Functor>> callbacks;
struct Nothing { };
std::shared_ptr<Nothing> alive = std::make_shared<Nothing>();
MutableValue(const MutableValue&) = delete;
MutableValue(MutableValue&&) = delete;
template <typename... Args>
explicit MutableValue(Args&&... args) noexcept
requires std::constructible_from<T, Args&&...>
: value { std::forward<Args>(args)... } { }
constexpr auto get() const noexcept -> T { return value; }
constexpr operator T() const noexcept { return get(); }
template <typename U>
auto set(U&& u) noexcept -> void
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
{
value = std::forward<U>(u);
for (const auto& [_, f] : callbacks)
f->update(value);
}
template <typename U>
auto set_silent(U&& u) noexcept -> void
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
{
value = std::forward<U>(u);
}
template <typename U>
auto operator=(U&& u) noexcept -> void
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
{
set(std::forward<U>(u));
}
};
template <class P, typename T>
struct MutableForward final : public P {
MutableValue<T>& mutable_value;
explicit MutableForward(MutableValue<T>& m) noexcept
requires std::constructible_from<P, T>
: mutable_value { m }
, P { m.get() } { }
template <template <typename> class Smart>
explicit MutableForward(const Smart<MutableValue<T>>& m) noexcept
requires std::constructible_from<P, T>
: MutableForward { *m } { }
explicit MutableForward(const P&, MutableValue<T>& m) noexcept
requires std::constructible_from<P, T>
: MutableForward { m } { }
template <template <typename> class Smart>
explicit MutableForward(const P&, const Smart<MutableValue<T>>& m) noexcept
requires std::constructible_from<P, T>
: MutableForward { m } { }
auto attach_callback_to_mutable(auto& widget) noexcept {
struct Functor : MutableValue<T>::Functor {
decltype(widget) w;
Functor(decltype(w) w) noexcept
: w { w } { }
~Functor() noexcept = default;
auto update(const T& value) -> void override {
w.apply(P { value });
w.update();
}
};
auto functor = std::make_unique<Functor>(widget);
mutable_value.callbacks[&widget] = std::move(functor);
auto alive = std::weak_ptr { mutable_value.alive };
QObject::connect(&widget, &QObject::destroyed, [this, alive](auto* key) {
if (alive.lock()) mutable_value.callbacks.erase(key);
});
}
auto apply(auto& widget) noexcept -> void {
P::apply(widget);
attach_callback_to_mutable(widget);
}
};
template <typename T, typename F>
struct MutableTransform : widget::pro::Token {
F apply_function;
MutableValue<T>& mutable_value;
explicit MutableTransform(F f, MutableValue<T>& t) noexcept
: mutable_value { t }
, apply_function { std::move(f) } { }
template <template <typename> class Smart>
explicit MutableTransform(F f, const Smart<MutableValue<T>>& m) noexcept
: MutableTransform { std::move(f), *m } { }
auto attach_callback_to_mutable(auto& widget) noexcept {
struct Functor : MutableValue<T>::Functor {
decltype(widget) w;
F apply_function;
Functor(decltype(widget) w, F f) noexcept
: w { w }
, apply_function { std::move(f) } { }
~Functor() noexcept = default;
auto update(const T& value) -> void override {
apply_function(w, value);
w.update();
}
};
auto functor = std::make_unique<Functor>(widget, std::move(apply_function));
mutable_value.callbacks[&widget] = std::move(functor);
auto alive = std::weak_ptr { mutable_value.alive };
QObject::connect(&widget, &QObject::destroyed, [this, alive](auto* key) {
if (alive.lock()) mutable_value.callbacks.erase(key);
});
}
auto apply(auto& widget) noexcept -> void {
apply_function(widget, mutable_value.get());
attach_callback_to_mutable(widget);
}
};
using MutableQString = MutableValue<QString>;
using MutableDouble = MutableValue<double>;
using MutableFloat = MutableValue<float>;
using MutableUInt8 = MutableValue<uint8_t>;
using MutableUInt16 = MutableValue<uint16_t>;
using MutableInt8 = MutableValue<int8_t>;
using MutableInt16 = MutableValue<int16_t>;
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include <functional>
#include <qwidget.h>
namespace creeper {
/// @brief 响应式属性包装器,允许属性变更后自动同步到所有已绑定组件。
///
/// 该模板用于包装具备 apply 方法的属性类型,使其具备响应式能力。
/// 通过 Mutable 包装后的属性可以被多个组件共享,
/// 当属性发生变更时,所有通过 Mutable 绑定的组件会自动更新自身属性,
/// 达到类似“全局属性联动”的效果。
///
/// Usage
/// auto mutable_size = Mutable{widget::pro::FixedSize{100, 100}};
/// auto card_1 = OutlinedCard{mutable_size};
/// auto card_2 = OutlinedCard{mutable_size};
/// // 修改 mutable_sizecard1/card2 会同步变化
/// mutable_size = {mutable_size.width() + 10, mutable_size.height() + 10};
///
/// @note
/// 1. 不支持将 Mutable 实例放入 std::tuple 进行批量 apply
/// 必须显式调用 apply 绑定组件。
/// 若允许混入 tuple容易导致属性的响应式副作用被隐式传播
/// 使得属性绑定和变更难以追踪,带来较大维护隐患。
/// 显式 apply 可以让响应式属性的边界清晰可控,
/// 便于代码审查和后期维护。
///
/// 2. Mutable 不支持拷贝和移动构造,需确保唯一性和生命周期安全。
///
/// 3. 绑定对象被销毁时,内部回调会自动解绑,无内存泄漏风险。
///
/// @date 2025.7.28
template <class T>
struct Mutable : public T {
public:
explicit Mutable(const T& t) noexcept
: T { t } { }
explicit Mutable(std::constructible_from<T> auto&&... args) noexcept
: T { std::forward<decltype(args)>(args)... } { }
Mutable(const Mutable&) = delete;
Mutable(Mutable&&) = delete;
auto apply(auto& self) const noexcept -> void {
T::apply(self);
Mutable::bind(self);
}
auto bind(auto& self) const noexcept -> void {
callbacks_.insert({
&self,
[&](const T& pro) {
pro.apply(self);
self.update();
},
});
QObject::connect(&self, &QObject::destroyed, //
[pointer = &self, this] { callbacks_.erase(pointer); });
}
template <typename U>
auto set(U&& u) noexcept -> Mutable<T>&
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
{
static_cast<T&>(*this) = std::forward<U>(u);
for (const auto& [_, f] : callbacks_)
f(*this);
return *this;
}
template <typename U>
auto operator=(U&& u) noexcept -> Mutable<T>&
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
{
return set(std::forward<U>(u));
}
private:
mutable std::unordered_map<QWidget*, std::function<void(const T&)>> callbacks_;
};
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <memory>
#define CREEPER_PIMPL_DEFINITION(Class) \
public: \
Class(); \
~Class(); \
Class(const Class&) = delete; \
Class& operator=(const Class&) = delete; \
\
private: \
struct Impl; \
std::unique_ptr<Impl> pimpl;
namespace creeper::internal {
inline void use_std_unique_ptr() { auto unique_ptr = std::unique_ptr<int> {}; }
}

View File

@@ -0,0 +1,162 @@
#pragma once
#include <tuple>
#include <utility>
// 少量样板代码的生成宏是可以接受的,
// 等 concept 可以作为参数的那一天,这个宏就可以废弃了
#define CREEPER_DEFINE_CHECKER(TRAIT) \
struct checker final { \
template <class T> \
static constexpr bool result = TRAIT<T>; \
};
namespace creeper {
/// CHECKER
template <class T>
concept checker_trait = requires {
{ T::template result<void> };
};
template <checker_trait... Ts>
struct CheckerOr {
template <class T>
static constexpr bool result = (Ts::template result<T> || ...);
};
template <checker_trait... Ts>
struct CheckerAnd {
template <class T>
static constexpr bool result = (Ts::template result<T> && ...);
};
/// PROP
template <class Token, typename T, auto interface>
struct SetterProp : Token {
T value;
constexpr explicit SetterProp(T value) noexcept
: value { std::move(value) } { }
constexpr explicit operator T(this auto&& self) noexcept { return self.value; }
template <typename O>
auto operator=(O&& other) noexcept -> SetterProp&
requires std::assignable_from<T&, O>
{
value = std::forward<O>(other);
return *this;
}
auto apply(auto& self) const noexcept -> void
requires requires { interface(self, std::declval<T>()); }
{
interface(self, value);
}
};
template <class Token, typename T, auto interface>
struct DerivedProp : T, Token {
using T::T;
template <typename... Args>
requires std::constructible_from<T, Args&&...>
constexpr explicit DerivedProp(Args&&... args)
: T(std::forward<Args>(args)...) { }
template <typename U>
requires requires(T& t, U&& u) { t = std::forward<U>(u); }
auto operator=(U&& value) -> DerivedProp& {
T::operator=(std::forward<U>(value));
return *this;
}
auto apply(this auto const& self, auto& widget) noexcept -> void {
interface(widget, static_cast<const T&>(self));
}
};
template <class Token, auto interface>
struct ActionProp : Token {
auto apply(auto& self) const noexcept -> void
requires requires { interface(self); }
{
interface(self);
}
};
/// @brief
/// 声明式包装,非侵入式实现 Setter 的声明式化
///
/// 该包装器支持将 std::tuple 与单个 prop 混合传入,不受顺序限制。内部通过
/// helper 检查 tuple 中的所有元素,递归调用 apply 将属性应用到 底层 Widget。
/// 利用 CheckerT 延迟模板实例化,在 concept 层面约束属性类型, 从而避免因
/// 递归参数展开而产生的海量且难以定位的模板错误。
///
/// @tparam W
/// 需要被包装的组件类型。
///
/// @tparam checker
/// 用于延迟模板实例化的“检查器”类型模板。典型形式如下:
/// struct checker final {
/// template <class T> struct result {
/// static constexpr auto v = concept<T>;
/// };
/// };
///
/// @note
/// 在不符合 checker 要求的类型被传入时,会在 concept 约束阶段直接报错,
/// 提供简洁且精准的编译期错误提示,避免编译器自动展开大量构造函数
/// 导致的冗长错误栈,但编译期报错信息依旧不友好。
///
template <class W, checker_trait checker>
struct Declarative : public W {
private:
// For helpe check single props
// 这里没法子塞一个 static_assert无论如何这里都会被尝试错误地实例化
template <typename T, class checker_>
static constexpr auto impl_props_trait = checker_::template result<std::remove_cvref_t<T>>;
// For help check tuple of props
// 使用 SFINAE 真是抱歉,没找到方便处理 tuple 的方法真是不好意思呢
template <typename T, class checker_>
static constexpr auto impl_tuple_trait = false;
template <typename... Ts, class checker_>
static constexpr auto impl_tuple_trait<std::tuple<Ts...>, checker_> =
(impl_props_trait<Ts, checker_> && ...);
public:
/* Export widget type */
using Widget = W;
/* Export checker type */
using Checker = checker;
/* For check props */
template <class T>
static constexpr auto props_trait = impl_props_trait<std::remove_cvref_t<T>, checker>;
/* For check tuple */
template <class T>
static constexpr auto tuple_trait = impl_tuple_trait<std::remove_cvref_t<T>, checker>;
public:
// 铁血的,热血的,冷血的声明式构造接口
explicit Declarative(auto&&... props) noexcept
requires((props_trait<decltype(props)> || tuple_trait<decltype(props)>) && ...)
: W {} {
(apply(std::forward<decltype(props)>(props)), ...);
}
auto apply(auto&& tuple) noexcept -> void
requires tuple_trait<decltype(tuple)>
{
std::apply([this](auto&&... args) { (apply(std::forward<decltype(args)>(args)), ...); },
std::forward<decltype(tuple)>(tuple));
}
auto apply(auto&& prop) noexcept -> void
requires props_trait<decltype(prop)>
{
std::forward<decltype(prop)>(prop).apply(*this);
}
};
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include <memory>
namespace creeper::util {
template <typename T> class Singleton {
public:
static T& instance();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
protected:
struct token { };
Singleton() = default;
};
template <typename T> inline T& Singleton<T>::instance() {
static const std::unique_ptr<T> instance { new T { token {} } };
return *instance;
}
}

View File

@@ -0,0 +1,177 @@
#pragma once
#include "creeper-qt/utility/wrapper/common.hh"
#include "creeper-qt/utility/wrapper/property.hh"
#include <qgraphicseffect.h>
#include <qscreen.h>
namespace creeper::widget::pro {
using Token = common::Token<QWidget>;
using MinimumWidth = SetterProp<Token, int, [](auto& self, int v) { self.setMinimumWidth(v); }>;
using MaximumWidth = SetterProp<Token, int, [](auto& self, int v) { self.setMaximumWidth(v); }>;
using FixedWidth = SetterProp<Token, int, [](auto& self, int v) { self.setFixedWidth(v); }>;
using MinimumHeight = SetterProp<Token, int, [](auto& self, int v) { self.setMinimumHeight(v); }>;
using MaximumHeight = SetterProp<Token, int, [](auto& self, int v) { self.setMaximumHeight(v); }>;
using FixedHeight = SetterProp<Token, int, [](auto& self, int v) { self.setFixedHeight(v); }>;
using LayoutDirection = SetterProp<Token, Qt::LayoutDirection,
[](auto& self, Qt::LayoutDirection v) { self.setLayoutDirection(v); }>;
using BackgroundRole = SetterProp<Token, QPalette::ColorRole,
[](auto& self, QPalette::ColorRole v) { self.setBackgroundRole(v); }>;
using ForegroundRole = SetterProp<Token, QPalette::ColorRole,
[](auto& self, QPalette::ColorRole v) { self.setForegroundRole(v); }>;
using ClearMask = SetterProp<Token, std::nullptr_t, [](auto& self, auto) { self.clearMask(); }>;
using GraphicsEffect = SetterProp<Token, QGraphicsEffect*,
[](auto& self, QGraphicsEffect* v) { self.setGraphicsEffect(v); }>;
struct WindowFlag : Token {
Qt::WindowType type;
bool on;
explicit WindowFlag(Qt::WindowType type, bool on = true)
: type { type }
, on { on } { }
void apply(QWidget& widget) const { widget.setWindowFlag(type, on); }
};
using WindowFlags = SetterProp<Token, Qt::WindowFlags,
[](auto& self, Qt::WindowFlags v) { self.setWindowFlags(v); }>;
using WindowOpacity =
SetterProp<Token, double, [](auto& self, double v) { self.setWindowOpacity(v); }>;
using Parent = SetterProp<Token, QWidget*, [](auto& self, QWidget* v) { self.setParent(v); }>;
using Child = SetterProp<Token, QWidget*, [](auto& self, QWidget* v) { v->setParent(&self); }>;
using MinimumSize =
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setMinimumSize(v); }>;
using MaximumSize =
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setMaximumSize(v); }>;
using SizeIncrement =
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setSizeIncrement(v); }>;
using BaseSize = DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setBaseSize(v); }>;
using FixedSize =
DerivedProp<Token, QSize, [](auto& self, const QSize& v) { self.setFixedSize(v); }>;
using Font = DerivedProp<Token, QFont, [](auto& self, const QFont& v) { self.setFont(v); }>;
using BitmapMask =
DerivedProp<Token, QBitmap, [](auto& self, const QBitmap& v) { self.setMask(v); }>;
using RegionMask =
DerivedProp<Token, QRegion, [](auto& self, const QRegion& v) { self.setMask(v); }>;
using WindowIcon =
DerivedProp<Token, QIcon, [](auto& self, const QIcon& v) { self.setWindowIcon(v); }>;
using WindowIconText =
DerivedProp<Token, QString, [](auto& self, const QString& v) { self.setWindowIconText(v); }>;
using WindowRole =
DerivedProp<Token, QString, [](auto& self, const QString& v) { self.setWindowRole(v); }>;
using WindowFilePath =
DerivedProp<Token, QString, [](auto& self, const QString& v) { self.setWindowFilePath(v); }>;
using ToolTip =
DerivedProp<Token, QString, [](auto& self, const QString& tip) { self.setToolTip(tip); }>;
struct MoveCenter : Token {
auto apply(QWidget& self) const noexcept -> void {
const auto screen = self.screen();
const auto screen_geometry = screen->availableGeometry();
const auto screen_width = screen_geometry.width();
const auto screen_height = screen_geometry.height();
const auto widget_geometry = self.geometry();
const auto widget_width = widget_geometry.width();
const auto widget_height = widget_geometry.height();
const auto x = (screen_width - widget_width) / 2;
const auto y = (screen_height - widget_height) / 2;
self.move(x, y);
}
};
struct SizePolicy : Token {
QSizePolicy::Policy v, h;
explicit SizePolicy(QSizePolicy::Policy policy)
: v { policy }
, h { policy } { }
explicit SizePolicy(QSizePolicy::Policy v, QSizePolicy::Policy h)
: v { v }
, h { h } { }
void apply(QWidget& self) const { self.setSizePolicy(h, v); }
};
/// @note 该属性本质是转发构造,有 new 的行为
template <class T>
struct Layout : Token {
T* layout_;
explicit Layout(T* pointer) noexcept
requires std::convertible_to<decltype(pointer), QLayout*>
: layout_ { pointer } { }
explicit Layout(auto&&... args)
requires std::constructible_from<T, decltype(args)...>
: layout_ { new T { std::forward<decltype(args)>(args)... } } { }
void apply(QWidget& widget) const { widget.setLayout(layout_); }
};
/// @brief 绑定裸指针,为了避免赋值语句
///
/// 一般情况下需要绑定指针:
///
/// 假设下面是一个 layout 的内部
/// clang-format 会在下面的赋值符号后换行
/// @code
/// auto widget = (Widget*)nullptr;
/// ......
/// { widget =
/// new Widget {
/// ......
/// } },
/// ......
/// @endcode
///
/// 利用赋值语句的返回特性将该组件返回给 layout同时完成赋值
/// 但这样会多一层缩进,为保证构造配置的简洁和同一,可以使用该包装:
///
/// @code
/// ......
/// { new Widget {
/// pro::Bind { widget },
/// } },
/// ......
/// @endcode
///
/// @tparam Final 需要绑定的组件类型(自动推导,无需显式指定)
///
/// @date 2025-06-19
template <class Final>
struct Bind : Token {
Final*& widget;
explicit Bind(Final*& widget) noexcept
requires std::is_pointer<Final*>::value
: widget(widget) { }
void apply(Final& self) const noexcept { widget = &self; }
};
// 传入一个方法用来辅助构造,在没有想要的接口时用这个吧
template <typename Lambda>
struct Apply : Token {
Lambda lambda;
explicit Apply(Lambda lambda) noexcept
: lambda { lambda } { }
auto apply(auto& self) const noexcept -> void {
if constexpr (std::invocable<Lambda>) lambda();
if constexpr (std::invocable<Lambda, decltype(self)>) lambda(self);
}
};
template <typename T>
concept trait = std::derived_from<T, Token>;
CREEPER_DEFINE_CHECKER(trait);
}